From mdounin at mdounin.ru Wed May 1 00:57:20 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 03:57:20 +0300 Subject: [PATCH] Tests: fixed auth_basic.t on OpenBSD Message-ID: # HG changeset patch # User Maxim Dounin # Date 1714524954 -10800 # Wed May 01 03:55:54 2024 +0300 # Node ID cee70d7876a81d3d44f862fc551324fdd766bf8d # Parent fe6f22da53ec760f7ab138d1d32b7a03ea7bdea3 Tests: fixed auth_basic.t on OpenBSD. OpenBSD does not provide any crypt() schemes except bcrypt-based "$2" anymore. As such, relevant tests are now skipped not for win32 only, but based on crypt() results. Note that just testing crypt('password', 'salt') is not enough, since Perl on win32 provides its own crypt() implementation, which is able to handle traditional DES crypt(), but rejects "$1$". diff --git a/auth_basic.t b/auth_basic.t --- a/auth_basic.t +++ b/auth_basic.t @@ -86,7 +86,9 @@ like(http_get('/'), qr!401 Unauthorized! SKIP: { -skip 'no crypt on win32', 5 if $^O eq 'MSWin32'; +skip 'no crypt', 5 + if not crypt('password', 'salt') + or not crypt('password', '$1$salt$'); like(http_get_auth('/', 'crypt', 'password'), qr!SEETHIS!, 'normal crypt'); unlike(http_get_auth('/', 'crypt', '123'), qr!SEETHIS!, 'normal wrong'); From mdounin at mdounin.ru Wed May 1 01:01:46 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 04:01:46 +0300 Subject: [PATCH 0 of 5] macOS test improvements Message-ID: Hello! The following patch series improves testing on macOS with OS-provided Net::SSLeay, which is compiled with LibreSSL. The following issues were identified and addressed: - Hangs, likely due to SSL_shutdown() misbehaviour. These seems to be fixed in recent LibreSSL versions, but macOS ships LibreSSL 3.3.6, which is a bit outdated. - TLSv1.3 ciphers naming, which used to be different in LibreSSL before 3.5.0. - No support for TLSv1.3 session reuse in LibreSSL. - No support for signature algorithms customization in LibreSSL. - No support for reading CA lists with TLSv1.3 in LibreSSL. Most of the issues seems to match those already handled with LibreSSL on the server side. Notable exception is hangs, but those were easy enough to work around. Review and testing appreciated. -- Maxim Dounin From mdounin at mdounin.ru Wed May 1 01:01:47 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 04:01:47 +0300 Subject: [PATCH 1 of 5] Tests: fixed hangs with Net::SSLeay with LibreSSL on macOS In-Reply-To: References: Message-ID: <3226c8c9a58a97be60cf.1714525307@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1714514801 -10800 # Wed May 01 01:06:41 2024 +0300 # Node ID 3226c8c9a58a97be60cf8b651ac3a48b18d61e82 # Parent fe6f22da53ec760f7ab138d1d32b7a03ea7bdea3 Tests: fixed hangs with Net::SSLeay with LibreSSL on macOS. On macOS (as seen on 14.4.1, latest at the moment), Net::SSLeay compiled with LibreSSL is shipped with the OS (Net::SSLeay 1.88, LibreSSL 3.3.6). And for some reason mail_ssl_conf_command.t and stream_ssl_conf_command.t tests hang after the test, waiting indefinitely in reused SSL socket close(). This seems to be an LibreSSL bug in SSL_shutdown() (at least a couple was fixed in LibreSSL 3.5.0), probably related to session reuse (which do work in these particular tests due to TLSv1.2 explicitly used for testing). Still, calling close(SSL_no_shutdown => 1) explicitly is a trivial workaround, and hence it is implemented. diff --git a/mail_ssl_conf_command.t b/mail_ssl_conf_command.t --- a/mail_ssl_conf_command.t +++ b/mail_ssl_conf_command.t @@ -103,6 +103,8 @@ like($s->socket()->dump_peer_certificate ); ok($s->socket()->get_session_reused(), 'SessionTicket'); +$s->socket()->close(SSL_no_shutdown => 1); + $s = Test::Nginx::IMAP->new( SSL => 1, SSL_cipher_list => diff --git a/stream_ssl_conf_command.t b/stream_ssl_conf_command.t --- a/stream_ssl_conf_command.t +++ b/stream_ssl_conf_command.t @@ -104,6 +104,8 @@ like($s->socket()->dump_peer_certificate ); ok($s->socket()->get_session_reused(), 'SessionTicket'); +$s->socket()->close(SSL_no_shutdown => 1); + $s = stream( PeerAddr => '127.0.0.1:' . port(8443), SSL => 1, From mdounin at mdounin.ru Wed May 1 01:01:48 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 04:01:48 +0300 Subject: [PATCH 2 of 5] Tests: relaxed mail_imap_ssl.t cipher matching In-Reply-To: References: Message-ID: <9910e3aa527abb558fbc.1714525308@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1714514801 -10800 # Wed May 01 01:06:41 2024 +0300 # Node ID 9910e3aa527abb558fbcd6ea54af9b22f058d361 # Parent 3226c8c9a58a97be60cf8b651ac3a48b18d61e82 Tests: relaxed mail_imap_ssl.t cipher matching. Previously, exact match between cipher name in the log and the one from IO::Socket:SSL was needed, which might not be the case if nginx and Net::SSLeay are compiled with different SSL libraries, notably LibreSSL (which uses names like AEAD-AES256-GCM-SHA384 till 3.5.0), and OpenSSL or BoringSSL (which use TLS_AES_256_GCM_SHA384). In particular, this affects macOS, where Net::SSLeay compiled with LibreSSL 3.3.6 is shipped with the OS, while nginx is likely to be compiled with OpenSSL. Fix is to not require exact match but instead accept properly looking names as checked by a regular expression, similarly to how it is already tested in ssl.t and stream_ssl_variables.t. diff --git a/mail_imap_ssl.t b/mail_imap_ssl.t --- a/mail_imap_ssl.t +++ b/mail_imap_ssl.t @@ -29,7 +29,7 @@ select STDOUT; $| = 1; local $SIG{PIPE} = 'IGNORE'; my $t = Test::Nginx->new() - ->has(qw/mail mail_ssl imap http rewrite socket_ssl_sslversion/) + ->has(qw/mail mail_ssl imap http rewrite socket_ssl/) ->has_daemon('openssl')->plan(13) ->write_file_expand('nginx.conf', <<'EOF'); @@ -202,15 +202,6 @@ my $s = Test::Nginx::IMAP->new(); $s->send('1 AUTHENTICATE PLAIN ' . $cred->("s5")); $s->read(); -# Auth-SSL-Protocol and Auth-SSL-Cipher headers - -my ($cipher, $sslversion); - -$s = Test::Nginx::IMAP->new(SSL => 1); -$cipher = $s->socket()->get_cipher(); -$sslversion = $s->socket()->get_sslversion(); -$sslversion =~ s/_/./; - undef $s; # test auth_http request header fields with access_log @@ -229,6 +220,6 @@ like($f, qr!^on:SUCCESS:(/?CN=3.example. 'log - trusted cert'); $f = $t->read_file('auth2.log'); -like($f, qr|^$cipher:$sslversion$|m, 'log - cipher sslversion'); +like($f, qr/^[\w-]+:(TLS|SSL)v[\d.]+$/m, 'log - cipher sslversion'); ############################################################################### From mdounin at mdounin.ru Wed May 1 01:01:49 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 04:01:49 +0300 Subject: [PATCH 3 of 5] Tests: session reuse handling with Net::SSLeay with LibreSSL In-Reply-To: References: Message-ID: <68b3d0906228ccb5d6f7.1714525309@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1714514801 -10800 # Wed May 01 01:06:41 2024 +0300 # Node ID 68b3d0906228ccb5d6f721d776bf68cd6c7fbc3f # Parent 9910e3aa527abb558fbcd6ea54af9b22f058d361 Tests: session reuse handling with Net::SSLeay with LibreSSL. LibreSSL does not support session reuse with TLSv1.3, and this causes various test failures if Net::SSLeay is compiled with LibreSSL. Notably, this affects macOS, where Net::SSLeay compiled with LibreSSL is shipped with the OS. Fix is to mark relevant tests as TODO if Net::SSLeay is compiled with LibreSSL, similarly to what we already do for LibreSSL on the server side. diff --git a/mail_ssl_session_reuse.t b/mail_ssl_session_reuse.t --- a/mail_ssl_session_reuse.t +++ b/mail_ssl_session_reuse.t @@ -140,6 +140,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); is(test_reuse(8993), 1, 'tickets reused'); is(test_reuse(8994), 1, 'tickets and cache reused'); diff --git a/ssl.t b/ssl.t --- a/ssl.t +++ b/ssl.t @@ -186,6 +186,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); like(get('/', 8085, $ctx), qr/^body r$/m, 'session reused'); diff --git a/ssl_certificate.t b/ssl_certificate.t --- a/ssl_certificate.t +++ b/ssl_certificate.t @@ -171,6 +171,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $Net::SSLeay::VERSION < 1.88 && test_tls13(); local $TODO = 'no TLSv1.3 sessions, old IO::Socket::SSL' if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); +local $TODO = 'not TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); like(get('default', 8080, $s), qr/default:r/, 'session reused'); diff --git a/ssl_ocsp.t b/ssl_ocsp.t --- a/ssl_ocsp.t +++ b/ssl_ocsp.t @@ -361,6 +361,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); like(get('ec-end', ses => $s), qr/200 OK.*SUCCESS:r/s, 'session reused'); @@ -393,6 +395,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); like(get('ec-end', ses => $s), qr/400 Bad.*FAILED:certificate revoked:r/s, 'session reused - revoked'); diff --git a/ssl_session_reuse.t b/ssl_session_reuse.t --- a/ssl_session_reuse.t +++ b/ssl_session_reuse.t @@ -167,6 +167,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); is(test_reuse(8443), 1, 'tickets reused'); is(test_reuse(8444), 1, 'tickets and cache reused'); diff --git a/ssl_session_ticket_key.t b/ssl_session_ticket_key.t --- a/ssl_session_ticket_key.t +++ b/ssl_session_ticket_key.t @@ -105,6 +105,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); cmp_ok(get_ticket_key_name(), 'ne', $key, 'ticket key next'); diff --git a/ssl_sni.t b/ssl_sni.t --- a/ssl_sni.t +++ b/ssl_sni.t @@ -128,6 +128,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); like(get('/name', 'localhost', $ctx), qr/^r:localhost$/m, 'ssl server name - reused'); diff --git a/ssl_sni_sessions.t b/ssl_sni_sessions.t --- a/ssl_sni_sessions.t +++ b/ssl_sni_sessions.t @@ -116,6 +116,8 @@ plan(skip_all => 'no TLSv1.3 sessions, o if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); plan(skip_all => 'no TLSv1.3 sessions in LibreSSL') if $t->has_module('LibreSSL') && test_tls13(); +plan(skip_all => 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)') + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); plan(skip_all => 'no TLS 1.3 session cache in BoringSSL') if $t->has_module('BoringSSL') && test_tls13(); diff --git a/stream_ssl_certificate.t b/stream_ssl_certificate.t --- a/stream_ssl_certificate.t +++ b/stream_ssl_certificate.t @@ -148,6 +148,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $Net::SSLeay::VERSION < 1.88 && test_tls13(); local $TODO = 'no TLSv1.3 sessions, old IO::Socket::SSL' if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); like(get('default', 8080, $s), qr/default:r/, 'session reused'); diff --git a/stream_ssl_session_reuse.t b/stream_ssl_session_reuse.t --- a/stream_ssl_session_reuse.t +++ b/stream_ssl_session_reuse.t @@ -144,6 +144,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); is(test_reuse(8443), 1, 'tickets reused'); is(test_reuse(8444), 1, 'tickets and cache reused'); diff --git a/stream_ssl_variables.t b/stream_ssl_variables.t --- a/stream_ssl_variables.t +++ b/stream_ssl_variables.t @@ -98,6 +98,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); $s = stream( PeerAddr => '127.0.0.1:' . port(8443), From mdounin at mdounin.ru Wed May 1 01:01:50 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 04:01:50 +0300 Subject: [PATCH 4 of 5] Tests: sigalgs handling with Net::SSLeay with LibreSSL In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1714514801 -10800 # Wed May 01 01:06:41 2024 +0300 # Node ID c271d34326938c0948edac4061a2290922200188 # Parent 68b3d0906228ccb5d6f721d776bf68cd6c7fbc3f Tests: sigalgs handling with Net::SSLeay with LibreSSL. LibreSSL does not provide a way to configure signature algorithms, and this makes it impossible to request a particular server certificate when using TLSv1.3. As such, relevant tests fail if Net::SSLeay is compiled with LibreSSL. Notably, this affects macOS, where Net::SSLeay compiled with LibreSSL is shipped with the OS. Fix is to mark relevant tests as TODO if Net:SSLeay is compiled with LibreSSL, similarly to what we already do for sigalg issues in LibreSSL on the server side. diff --git a/ssl_certificates.t b/ssl_certificates.t --- a/ssl_certificates.t +++ b/ssl_certificates.t @@ -99,8 +99,14 @@ like(cert('RSA'), qr/CN=rsa/, 'ssl cert } +TODO: { +local $TODO = 'no TLSv1.3 sigalgs in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); + like(cert('ECDSA'), qr/CN=ec/, 'ssl cert ECDSA'); +} + ############################################################################### sub test_tls13 { diff --git a/ssl_stapling.t b/ssl_stapling.t --- a/ssl_stapling.t +++ b/ssl_stapling.t @@ -262,6 +262,8 @@ ok(!staple(8443, 'RSA'), 'staple revoked TODO: { local $TODO = 'broken TLSv1.3 sigalgs in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sigalgs in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); ok(staple(8443, 'ECDSA'), 'staple success'); @@ -272,6 +274,8 @@ ok(!staple(8444, 'RSA'), 'responder revo TODO: { local $TODO = 'broken TLSv1.3 sigalgs in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sigalgs in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); ok(staple(8444, 'ECDSA'), 'responder success'); @@ -288,7 +292,9 @@ ok(!staple(8449, 'ECDSA'), 'ocsp error') TODO: { local $TODO = 'broken TLSv1.3 sigalgs in LibreSSL' - if $t->has_module('LibreSSL') && test_tls13(); + if $t->has_module('LibreSSL') + && !Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") + && test_tls13(); like(`grep -F '[crit]' ${\($t->testdir())}/error.log`, qr/^$/s, 'no crit'); From mdounin at mdounin.ru Wed May 1 01:01:51 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 04:01:51 +0300 Subject: [PATCH 5 of 5] Tests: CA list handling with Net::SSLeay with LibreSSL In-Reply-To: References: Message-ID: <3b779799abd153ebcc79.1714525311@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1714514801 -10800 # Wed May 01 01:06:41 2024 +0300 # Node ID 3b779799abd153ebcc791be66604c50494b55c6c # Parent c271d34326938c0948edac4061a2290922200188 Tests: CA list handling with Net::SSLeay with LibreSSL. LibreSSL is not able to work with CA lists (certificate_authorities extension) when using TLSv1.3. As such, relevant tests fail if Net::SSLeay is compiled with LibreSSL. Notably, this affects macOS, where Net::SSLeay compiled with LibreSSL is shipped with the OS. Fix is to mark relevant tests as TODO if Net:SSLeay is compiled with LibreSSL, similarly to what we already do for TLSv1.3 CA list issues in LibreSSL on the server side. diff --git a/ssl_verify_client.t b/ssl_verify_client.t --- a/ssl_verify_client.t +++ b/ssl_verify_client.t @@ -158,6 +158,8 @@ skip 'Net::SSLeay version >= 1.36 requir TODO: { local $TODO = 'broken TLSv1.3 CA list in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 CA list in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); my $ca = join ' ', get('optional', '3.example.com'); is($ca, '/CN=2.example.com', 'no trusted sent'); diff --git a/stream_ssl_verify_client.t b/stream_ssl_verify_client.t --- a/stream_ssl_verify_client.t +++ b/stream_ssl_verify_client.t @@ -126,6 +126,8 @@ skip 'Net::SSLeay version >= 1.36 requir TODO: { local $TODO = 'broken TLSv1.3 CA list in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 CA list in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); my $ca = join ' ', get(8082, '3.example.com'); is($ca, '/CN=2.example.com', 'no trusted sent'); From mdounin at mdounin.ru Wed May 1 01:53:06 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 04:53:06 +0300 Subject: [PATCH 0 of 5] tests with old OpenSSL versions Message-ID: Hello! The following patch series fixes tests when running with old OpenSSL versions, notably before OpenSSL 1.0.2. While these versions aren't common nowadays, these are still supported by the code (minimum supported OpenSSL version is 0.9.8). Review and testing appreciated. -- Maxim Dounin From mdounin at mdounin.ru Wed May 1 01:53:07 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 04:53:07 +0300 Subject: [PATCH 1 of 5] Tests: adjusted stream_ssl_alpn.t to handle old SSL libraries In-Reply-To: References: Message-ID: <8566a3522573e6b59dea.1714528387@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1714526964 -10800 # Wed May 01 04:29:24 2024 +0300 # Node ID 8566a3522573e6b59dea2995c2d1c4c4524ecc86 # Parent fe6f22da53ec760f7ab138d1d32b7a03ea7bdea3 Tests: adjusted stream_ssl_alpn.t to handle old SSL libraries. The "ssl_alpn" directive of the stream module requires an SSL library with ALPN support, and startup fails if the directive is used in the configuration but ALPN support is not available (notably, with OpenSSL before 1.0.2). Restored try_run() to skip the test in this case. diff --git a/stream_ssl_alpn.t b/stream_ssl_alpn.t --- a/stream_ssl_alpn.t +++ b/stream_ssl_alpn.t @@ -71,7 +71,7 @@ foreach my $name ('localhost') { or die "Can't create certificate for $name: $!\n"; } -$t->run()->plan(6); +$t->try_run('no ssl_alpn')->plan(6); ############################################################################### From mdounin at mdounin.ru Wed May 1 01:53:08 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 04:53:08 +0300 Subject: [PATCH 2 of 5] Tests: relaxed SSL version used in testing In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1714527724 -10800 # Wed May 01 04:42:04 2024 +0300 # Node ID f1b147125456b5904e9a8d080c338e0207602f16 # Parent 8566a3522573e6b59dea2995c2d1c4c4524ecc86 Tests: relaxed SSL version used in testing. This ensures that tests can be properly run with old OpenSSL versions when using recent versions of IO::Socket::SSL (which defaults to TLS 1.2+ starting with IO::Socket:SSL version 2.082, and therefore not compatible with OpenSSL before 1.0.1). diff --git a/h2_ssl.t b/h2_ssl.t --- a/h2_ssl.t +++ b/h2_ssl.t @@ -154,6 +154,7 @@ sub get_ssl_socket { Proto => 'tcp', PeerAddr => '127.0.0.1', PeerPort => port(8080), + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_alpn_protocols => $alpn, SSL_error_trap => sub { die $_[1] } diff --git a/h2_ssl_verify_client.t b/h2_ssl_verify_client.t --- a/h2_ssl_verify_client.t +++ b/h2_ssl_verify_client.t @@ -112,6 +112,7 @@ sub get_ssl_socket { Proto => 'tcp', PeerAddr => '127.0.0.1', PeerPort => port(8080), + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_alpn_protocols => [ 'h2' ], SSL_hostname => $sni, diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -872,6 +872,7 @@ sub http_start($;%) { require IO::Socket::SSL; IO::Socket::SSL->start_SSL( $s, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), %extra diff --git a/lib/Test/Nginx/HTTP2.pm b/lib/Test/Nginx/HTTP2.pm --- a/lib/Test/Nginx/HTTP2.pm +++ b/lib/Test/Nginx/HTTP2.pm @@ -548,6 +548,7 @@ sub new_socket { ); require IO::Socket::SSL if $extra{'SSL'}; IO::Socket::SSL->start_SSL($s, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_npn_protocols => $npn ? [ $npn ] : undef, SSL_alpn_protocols => $alpn ? [ $alpn ] : undef, diff --git a/lib/Test/Nginx/IMAP.pm b/lib/Test/Nginx/IMAP.pm --- a/lib/Test/Nginx/IMAP.pm +++ b/lib/Test/Nginx/IMAP.pm @@ -38,6 +38,7 @@ sub new { require IO::Socket::SSL; IO::Socket::SSL->start_SSL( $self->{_socket}, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), @_ diff --git a/lib/Test/Nginx/POP3.pm b/lib/Test/Nginx/POP3.pm --- a/lib/Test/Nginx/POP3.pm +++ b/lib/Test/Nginx/POP3.pm @@ -38,6 +38,7 @@ sub new { require IO::Socket::SSL; IO::Socket::SSL->start_SSL( $self->{_socket}, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), @_ diff --git a/lib/Test/Nginx/SMTP.pm b/lib/Test/Nginx/SMTP.pm --- a/lib/Test/Nginx/SMTP.pm +++ b/lib/Test/Nginx/SMTP.pm @@ -38,6 +38,7 @@ sub new { require IO::Socket::SSL; IO::Socket::SSL->start_SSL( $self->{_socket}, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), @_ diff --git a/lib/Test/Nginx/Stream.pm b/lib/Test/Nginx/Stream.pm --- a/lib/Test/Nginx/Stream.pm +++ b/lib/Test/Nginx/Stream.pm @@ -54,6 +54,7 @@ sub new { require IO::Socket::SSL; IO::Socket::SSL->start_SSL( $self->{_socket}, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), @_ diff --git a/ssl.t b/ssl.t --- a/ssl.t +++ b/ssl.t @@ -313,6 +313,7 @@ sub cert { sub get_ssl_context { return IO::Socket::SSL::SSL_Context->new( + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_session_cache_size => 100 ); diff --git a/ssl_proxy_upgrade.t b/ssl_proxy_upgrade.t --- a/ssl_proxy_upgrade.t +++ b/ssl_proxy_upgrade.t @@ -170,6 +170,7 @@ sub upgrade_connect { my $s = IO::Socket::SSL->new( Proto => 'tcp', PeerAddr => '127.0.0.1:' . port(8080), + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), ) or die "Can't connect to nginx: $!\n"; diff --git a/ssl_sni.t b/ssl_sni.t --- a/ssl_sni.t +++ b/ssl_sni.t @@ -116,6 +116,7 @@ like(get_host('example.org', 'example.co # $ssl_server_name in sessions my $ctx = new IO::Socket::SSL::SSL_Context( + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_session_cache_size => 100); diff --git a/ssl_sni_sessions.t b/ssl_sni_sessions.t --- a/ssl_sni_sessions.t +++ b/ssl_sni_sessions.t @@ -157,6 +157,7 @@ like(get('tickets', 8444, $ctx), qr!tick sub get_ssl_context { return IO::Socket::SSL::SSL_Context->new( + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_session_cache_size => 100 ); diff --git a/stream_proxy_protocol_ssl.t b/stream_proxy_protocol_ssl.t --- a/stream_proxy_protocol_ssl.t +++ b/stream_proxy_protocol_ssl.t @@ -152,6 +152,7 @@ sub stream_daemon_ssl { eval { IO::Socket::SSL->start_SSL($client, SSL_server => 1, + SSL_version => 'SSLv23', SSL_cert_file => "$d/localhost.crt", SSL_key_file => "$d/localhost.key", SSL_error_trap => sub { die $_[1] } diff --git a/stream_ssl_realip.t b/stream_ssl_realip.t --- a/stream_ssl_realip.t +++ b/stream_ssl_realip.t @@ -133,6 +133,7 @@ sub pp_get { local $SIG{PIPE} = sub { die "sigpipe\n" }; alarm(8); IO::Socket::SSL->start_SSL($s, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_error_trap => sub { die $_[1] } ); From mdounin at mdounin.ru Wed May 1 01:53:09 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 04:53:09 +0300 Subject: [PATCH 3 of 5] Tests: guarded session ticket tests for old OpenSSL versions In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1714527782 -10800 # Wed May 01 04:43:02 2024 +0300 # Node ID c20055d3b8275fbc29f872ba8059d7b625be78c5 # Parent f1b147125456b5904e9a8d080c338e0207602f16 Tests: guarded session ticket tests for old OpenSSL versions. Much like SNI support, TLS session tickets are available starting with OpenSSL 0.9.8f if TLS extensions support is explicitly configured, and enabled by default since 0.9.8j. As such, SNI availability is checked to ensure TLS extensions support is compiled in. Additionally, the ssl_session_ticket_key.t tests for automatic ticket key rotation, which uses session ticket key callback, as introduced in OpenSSL 0.9.8h. diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -114,6 +114,7 @@ sub has_module($) { my %regex = ( sni => 'TLS SNI support enabled', + tickets => 'TLS SNI support enabled', mail => '--with-mail((?!\S)|=dynamic)', flv => '--with-http_flv_module', perl => '--with-http_perl_module', diff --git a/mail_ssl_session_reuse.t b/mail_ssl_session_reuse.t --- a/mail_ssl_session_reuse.t +++ b/mail_ssl_session_reuse.t @@ -141,7 +141,13 @@ local $TODO = 'no TLSv1.3 sessions, old local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +TODO: { +local $TODO = 'no session tickets' unless $t->has_module('tickets'); + is(test_reuse(8993), 1, 'tickets reused'); + +} + is(test_reuse(8994), 1, 'tickets and cache reused'); TODO: { diff --git a/ssl_session_reuse.t b/ssl_session_reuse.t --- a/ssl_session_reuse.t +++ b/ssl_session_reuse.t @@ -168,7 +168,13 @@ local $TODO = 'no TLSv1.3 sessions, old local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +TODO: { +local $TODO = 'no session tickets' unless $t->has_module('tickets'); + is(test_reuse(8443), 1, 'tickets reused'); + +} + is(test_reuse(8444), 1, 'tickets and cache reused'); TODO: { diff --git a/ssl_session_ticket_key.t b/ssl_session_ticket_key.t --- a/ssl_session_ticket_key.t +++ b/ssl_session_ticket_key.t @@ -27,7 +27,7 @@ plan(skip_all => 'Net::SSLeay version => eval { require IO::Socket::SSL; die if $IO::Socket::SSL::VERSION < 2.030; }; plan(skip_all => 'IO::Socket::SSL version => 2.030 required') if $@; -my $t = Test::Nginx->new()->has(qw/http http_ssl socket_ssl/) +my $t = Test::Nginx->new()->has(qw/http http_ssl tickets socket_ssl/) ->has_daemon('openssl')->plan(2) ->write_file_expand('nginx.conf', <<'EOF'); @@ -99,6 +99,8 @@ is(get_ticket_key_name(), $key, 'ticket select undef, undef, undef, 2.5; +local $TODO = 'no ticket key callback' + if $t->has_module('OpenSSL') and not $t->has_feature('openssl:0.9.8h'); local $TODO = 'no TLSv1.3 sessions, old Net::SSLeay' if $Net::SSLeay::VERSION < 1.88 && test_tls13(); local $TODO = 'no TLSv1.3 sessions, old IO::Socket::SSL' diff --git a/stream_ssl_session_reuse.t b/stream_ssl_session_reuse.t --- a/stream_ssl_session_reuse.t +++ b/stream_ssl_session_reuse.t @@ -145,7 +145,13 @@ local $TODO = 'no TLSv1.3 sessions, old local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +TODO: { +local $TODO = 'no session tickets' unless $t->has_module('tickets'); + is(test_reuse(8443), 1, 'tickets reused'); + +} + is(test_reuse(8444), 1, 'tickets and cache reused'); TODO: { From mdounin at mdounin.ru Wed May 1 01:53:10 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 04:53:10 +0300 Subject: [PATCH 4 of 5] Tests: guarded ssl_stapling.t for old OpenSSL versions In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1714527830 -10800 # Wed May 01 04:43:50 2024 +0300 # Node ID aaba4e48aa7d327e49dee8cca02cd1a7b9b6e0c3 # Parent c20055d3b8275fbc29f872ba8059d7b625be78c5 Tests: guarded ssl_stapling.t for old OpenSSL versions. Stapling requires the certificate status TLS extension. It is implemented in OpenSSL 0.9.8h and available if TLS extensions support is explicitly configured, and by default since 0.9.8j. To properly test if stapling is available, OpenSSL version is checked, and SNI availability is checked to ensure TLS extensions support is compiled in. diff --git a/ssl_stapling.t b/ssl_stapling.t --- a/ssl_stapling.t +++ b/ssl_stapling.t @@ -32,7 +32,12 @@ plan(skip_all => 'Net::SSLeay too old') eval { defined &IO::Socket::SSL::SSL_OCSP_TRY_STAPLE or die; }; plan(skip_all => 'IO::Socket::SSL too old') if $@; -plan(skip_all => 'no OCSP stapling') if $t->has_module('BoringSSL'); +plan(skip_all => 'no OCSP stapling') + if $t->has_module('BoringSSL'); +plan(skip_all => 'no OCSP stapling') + if $t->has_module('OpenSSL') and not $t->has_feature('openssl:0.9.8h'); +plan(skip_all => 'no OCSP stapling') + if not $t->has_module('sni'); $t->plan(10)->write_file_expand('nginx.conf', <<'EOF'); From mdounin at mdounin.ru Wed May 1 01:53:11 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 01 May 2024 04:53:11 +0300 Subject: [PATCH 5 of 5] Tests: guarded ssl_certificates.t for old OpenSSL versions In-Reply-To: References: Message-ID: <1d4089d4b7b14afef191.1714528391@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1714527846 -10800 # Wed May 01 04:44:06 2024 +0300 # Node ID 1d4089d4b7b14afef191fb6b28152f841d3eb2be # Parent aaba4e48aa7d327e49dee8cca02cd1a7b9b6e0c3 Tests: guarded ssl_certificates.t for old OpenSSL versions. The ssl_certificates.t test uses ECDSA ciphers, which are only available starting with OpenSSL 0.9.8b. In previous versions, notably in OpenSSL 0.9.8 and 0.9.8a, only non-standard example ciphers where available, which cannot be used with any other clients. The same applies to ssl_stapling.t, but it also requires the certificate status TLS extension, which is only available since OpenSSL 0.9.8h, and therefore no additional checks are needed. diff --git a/ssl_certificates.t b/ssl_certificates.t --- a/ssl_certificates.t +++ b/ssl_certificates.t @@ -25,7 +25,10 @@ select STDOUT; $| = 1; my $t = Test::Nginx->new()->has(qw/http http_ssl socket_ssl/) ->has_daemon('openssl'); -plan(skip_all => 'no multiple certificates') if $t->has_module('BoringSSL'); +plan(skip_all => 'no multiple certificates') + if $t->has_module('BoringSSL'); +plan(skip_all => 'no ECDSA support') + if $t->has_module('OpenSSL') and not $t->has_feature('openssl:0.9.8b'); $t->write_file_expand('nginx.conf', <<'EOF'); From mdounin at mdounin.ru Thu May 2 16:54:51 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Thu, 02 May 2024 19:54:51 +0300 Subject: [PATCH] Tests: improved compatibility when using recent "openssl" app Message-ID: <2578fe6a2d9e6b3a6206.1714668891@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1714668482 -10800 # Thu May 02 19:48:02 2024 +0300 # Node ID 2578fe6a2d9e6b3a6206d56f09e1aaeb9f740a17 # Parent fe6f22da53ec760f7ab138d1d32b7a03ea7bdea3 Tests: improved compatibility when using recent "openssl" app. Starting with OpenSSL 3.0, "openssl genrsa" generates encrypted keys in PKCS#8 format instead of previously used PKCS#1 format. Further, since OpenSSL 1.1.0 such keys are using PBKDF2 hmacWithSHA256. Such keys are not supported by old SSL libraries, notably by OpenSSL before 1.0.0 (OpenSSL 0.9.8 only supports hmacWithSHA1) and by BoringSSL before May 21, 2019 (support for hmacWithSHA256 was added in 302a4dee6c), and trying to load such keys into nginx compiled with an old SSL library results in "unsupported prf" errors. To facilitate testing with old SSL libraries, keys are now generated with "openssl genrsa -traditional" if the flag is available. diff --git a/grpc_ssl.t b/grpc_ssl.t --- a/grpc_ssl.t +++ b/grpc_ssl.t @@ -103,6 +103,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('localhost') { system('openssl req -x509 -new ' @@ -114,7 +115,7 @@ foreach my $name ('localhost') { foreach my $name ('client') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/mail_ssl.t b/mail_ssl.t --- a/mail_ssl.t +++ b/mail_ssl.t @@ -115,10 +115,11 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('localhost', 'inherits') { system("openssl genrsa -out $d/$name.key -passout pass:localhost " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/proxy_ssl_certificate.t b/proxy_ssl_certificate.t --- a/proxy_ssl_certificate.t +++ b/proxy_ssl_certificate.t @@ -107,6 +107,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('1.example.com', '2.example.com') { system('openssl req -x509 -new ' @@ -118,7 +119,7 @@ foreach my $name ('1.example.com', '2.ex foreach my $name ('3.example.com') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/proxy_ssl_certificate_vars.t b/proxy_ssl_certificate_vars.t --- a/proxy_ssl_certificate_vars.t +++ b/proxy_ssl_certificate_vars.t @@ -106,6 +106,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('1.example.com', '2.example.com') { system('openssl req -x509 -new ' @@ -117,7 +118,7 @@ foreach my $name ('1.example.com', '2.ex foreach my $name ('3.example.com') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/ssl_certificate.t b/ssl_certificate.t --- a/ssl_certificate.t +++ b/ssl_certificate.t @@ -125,6 +125,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('one', 'two') { system('openssl req -x509 -new ' @@ -136,7 +137,7 @@ foreach my $name ('one', 'two') { foreach my $name ('pass') { system("openssl genrsa -out $d/$name.key -passout pass:pass " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create $name key: $!\n"; system("openssl req -x509 -new -config $d/openssl.conf " . "-subj /CN=$name/ -out $d/$name.crt -key $d/$name.key " diff --git a/ssl_password_file.t b/ssl_password_file.t --- a/ssl_password_file.t +++ b/ssl_password_file.t @@ -94,11 +94,13 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; + mkfifo("$d/password_fifo", 0700); foreach my $name ('localhost', 'inherits') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/stream_proxy_ssl_certificate.t b/stream_proxy_ssl_certificate.t --- a/stream_proxy_ssl_certificate.t +++ b/stream_proxy_ssl_certificate.t @@ -113,6 +113,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('1.example.com', '2.example.com') { system('openssl req -x509 -new ' @@ -124,7 +125,7 @@ foreach my $name ('1.example.com', '2.ex foreach my $name ('3.example.com') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/stream_proxy_ssl_certificate_vars.t b/stream_proxy_ssl_certificate_vars.t --- a/stream_proxy_ssl_certificate_vars.t +++ b/stream_proxy_ssl_certificate_vars.t @@ -120,6 +120,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('1.example.com', '2.example.com') { system('openssl req -x509 -new ' @@ -131,7 +132,7 @@ foreach my $name ('1.example.com', '2.ex foreach my $name ('3.example.com') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/stream_ssl.t b/stream_ssl.t --- a/stream_ssl.t +++ b/stream_ssl.t @@ -90,11 +90,13 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; + mkfifo("$d/password_fifo", 0700); foreach my $name ('localhost', 'inherits') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/stream_ssl_certificate.t b/stream_ssl_certificate.t --- a/stream_ssl_certificate.t +++ b/stream_ssl_certificate.t @@ -108,6 +108,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('one', 'two') { system('openssl req -x509 -new ' @@ -119,7 +120,7 @@ foreach my $name ('one', 'two') { foreach my $name ('pass') { system("openssl genrsa -out $d/$name.key -passout pass:pass " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create $name key: $!\n"; system("openssl req -x509 -new -config $d/openssl.conf " . "-subj /CN=$name/ -out $d/$name.crt -key $d/$name.key " diff --git a/uwsgi_ssl_certificate.t b/uwsgi_ssl_certificate.t --- a/uwsgi_ssl_certificate.t +++ b/uwsgi_ssl_certificate.t @@ -105,6 +105,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('1.example.com', '2.example.com') { system('openssl req -x509 -new ' @@ -116,7 +117,7 @@ foreach my $name ('1.example.com', '2.ex foreach my $name ('3.example.com') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/uwsgi_ssl_certificate_vars.t b/uwsgi_ssl_certificate_vars.t --- a/uwsgi_ssl_certificate_vars.t +++ b/uwsgi_ssl_certificate_vars.t @@ -104,6 +104,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('1.example.com', '2.example.com') { system('openssl req -x509 -new ' @@ -115,7 +116,7 @@ foreach my $name ('1.example.com', '2.ex foreach my $name ('3.example.com') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " From mdounin at mdounin.ru Thu May 2 16:59:10 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Thu, 02 May 2024 19:59:10 +0300 Subject: [PATCH] Tests: improved has_daemon() to drop "which" stderr Message-ID: # HG changeset patch # User Maxim Dounin # Date 1714669097 -10800 # Thu May 02 19:58:17 2024 +0300 # Node ID c25ada43ab4fb71ea1e8a73e59883fc00c24dd92 # Parent fe6f22da53ec760f7ab138d1d32b7a03ea7bdea3 Tests: improved has_daemon() to drop "which" stderr. On some platforms (notably OpenBSD) "which" prints an error if the command is not found. It is now dropped to produce less clutter during testing. diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -336,7 +336,7 @@ sub has_daemon($) { } Test::More::plan(skip_all => "$daemon not found") - unless `which $daemon`; + unless `which $daemon 2>/dev/null`; return $self; } From mdounin at mdounin.ru Fri May 3 00:11:05 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 03 May 2024 03:11:05 +0300 Subject: [PATCH 0 of 2] SSL errors logging level Message-ID: Hello! The following two patches adjust logging level of various SSL errors which can be triggered by clients, notably "SSL alert number N" (for all possible values of N) and "invalid alert". -- Maxim Dounin From mdounin at mdounin.ru Fri May 3 00:11:06 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 03 May 2024 03:11:06 +0300 Subject: [PATCH 1 of 2] SSL: logging level of all "SSL alert number N" errors In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1714695001 -10800 # Fri May 03 03:10:01 2024 +0300 # Node ID eded0170b9606af830a23da4f26e48bf6fe5c318 # Parent 388a801e9bb9ab0a44713c510d1337a9432fe2b7 SSL: logging level of all "SSL alert number N" errors. Errors about alerts received from peers are generated by OpenSSL by adding peer-provided alert description (from 0 to 255) to SSL_AD_REASON_OFFSET. All such errors, including ones for unknown alerts, are now logged at the "info" level, as these can be caused by a misbehaving client. diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -3590,32 +3590,8 @@ ngx_ssl_connection_error(ngx_connection_ #ifdef SSL_R_BAD_RECORD_TYPE || n == SSL_R_BAD_RECORD_TYPE /* 443 */ #endif - || n == 1000 /* SSL_R_SSLV3_ALERT_CLOSE_NOTIFY */ -#ifdef SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE - || n == SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE /* 1010 */ - || n == SSL_R_SSLV3_ALERT_BAD_RECORD_MAC /* 1020 */ - || n == SSL_R_TLSV1_ALERT_DECRYPTION_FAILED /* 1021 */ - || n == SSL_R_TLSV1_ALERT_RECORD_OVERFLOW /* 1022 */ - || n == SSL_R_SSLV3_ALERT_DECOMPRESSION_FAILURE /* 1030 */ - || n == SSL_R_SSLV3_ALERT_HANDSHAKE_FAILURE /* 1040 */ - || n == SSL_R_SSLV3_ALERT_NO_CERTIFICATE /* 1041 */ - || n == SSL_R_SSLV3_ALERT_BAD_CERTIFICATE /* 1042 */ - || n == SSL_R_SSLV3_ALERT_UNSUPPORTED_CERTIFICATE /* 1043 */ - || n == SSL_R_SSLV3_ALERT_CERTIFICATE_REVOKED /* 1044 */ - || n == SSL_R_SSLV3_ALERT_CERTIFICATE_EXPIRED /* 1045 */ - || n == SSL_R_SSLV3_ALERT_CERTIFICATE_UNKNOWN /* 1046 */ - || n == SSL_R_SSLV3_ALERT_ILLEGAL_PARAMETER /* 1047 */ - || n == SSL_R_TLSV1_ALERT_UNKNOWN_CA /* 1048 */ - || n == SSL_R_TLSV1_ALERT_ACCESS_DENIED /* 1049 */ - || n == SSL_R_TLSV1_ALERT_DECODE_ERROR /* 1050 */ - || n == SSL_R_TLSV1_ALERT_DECRYPT_ERROR /* 1051 */ - || n == SSL_R_TLSV1_ALERT_EXPORT_RESTRICTION /* 1060 */ - || n == SSL_R_TLSV1_ALERT_PROTOCOL_VERSION /* 1070 */ - || n == SSL_R_TLSV1_ALERT_INSUFFICIENT_SECURITY /* 1071 */ - || n == SSL_R_TLSV1_ALERT_INTERNAL_ERROR /* 1080 */ - || n == SSL_R_TLSV1_ALERT_USER_CANCELLED /* 1090 */ - || n == SSL_R_TLSV1_ALERT_NO_RENEGOTIATION /* 1100 */ -#endif + || (n >= SSL_AD_REASON_OFFSET /* 1000 */ + && n <= SSL_AD_REASON_OFFSET + 255) ) { switch (c->log_error) { From mdounin at mdounin.ru Fri May 3 00:11:07 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 03 May 2024 03:11:07 +0300 Subject: [PATCH 2 of 2] SSL: logging level of "invalid alert" errors In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1714695004 -10800 # Fri May 03 03:10:04 2024 +0300 # Node ID c8507f82ea4d0075313db01d784e5ec3caa55089 # Parent eded0170b9606af830a23da4f26e48bf6fe5c318 SSL: logging level of "invalid alert" errors. The SSL_R_INVALID_ALERT ("invalid alert") errors are reported by OpenSSL 1.1.1 or newer if the client sends a malformed alert. These errors are now logged at the "info" level. diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -3495,6 +3495,9 @@ ngx_ssl_connection_error(ngx_connection_ #ifdef SSL_R_PACKET_LENGTH_TOO_LONG || n == SSL_R_PACKET_LENGTH_TOO_LONG /* 198 */ #endif +#ifdef SSL_R_INVALID_ALERT + || n == SSL_R_INVALID_ALERT /* 205 */ +#endif || n == SSL_R_RECORD_LENGTH_MISMATCH /* 213 */ #ifdef SSL_R_TOO_MANY_WARNING_ALERTS || n == SSL_R_TOO_MANY_WARNING_ALERTS /* 220 */ From mdounin at mdounin.ru Fri May 3 00:26:34 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 03 May 2024 03:26:34 +0300 Subject: [PATCH] Fixed compilation on NetBSD 10 Message-ID: # HG changeset patch # User Maxim Dounin # Date 1714695523 -10800 # Fri May 03 03:18:43 2024 +0300 # Node ID ad458536f6e7f7fee8e0571c282db03894f500f1 # Parent 388a801e9bb9ab0a44713c510d1337a9432fe2b7 Fixed compilation on NetBSD 10. NetBSD 10 changed struct kevent's udata type from "intptr_t" to "void *", similarly to how it is defined on other systems. This breaks compilation, since intptr_t is assumed on NetBSD. Fix is to limit special intptr_t handling to NetBSD versions before 10.0. To simplify testing, relevant definitions are moved from configure to code (which seems to be the right thing anyway). diff --git a/auto/unix b/auto/unix --- a/auto/unix +++ b/auto/unix @@ -129,26 +129,6 @@ if test -z "$NGX_KQUEUE_CHECKED"; then fi -if [ "$NGX_SYSTEM" = "NetBSD" ]; then - - # NetBSD 2.0 incompatibly defines kevent.udata as "intptr_t" - - cat << END >> $NGX_AUTO_CONFIG_H - -#define NGX_KQUEUE_UDATA_T - -END - -else - cat << END >> $NGX_AUTO_CONFIG_H - -#define NGX_KQUEUE_UDATA_T (void *) - -END - -fi - - ngx_feature="crypt()" ngx_feature_name= ngx_feature_run=no diff --git a/src/os/unix/ngx_darwin_config.h b/src/os/unix/ngx_darwin_config.h --- a/src/os/unix/ngx_darwin_config.h +++ b/src/os/unix/ngx_darwin_config.h @@ -74,6 +74,7 @@ #if (NGX_HAVE_KQUEUE) #include +#define NGX_KQUEUE_UDATA_T (void *) #endif diff --git a/src/os/unix/ngx_freebsd_config.h b/src/os/unix/ngx_freebsd_config.h --- a/src/os/unix/ngx_freebsd_config.h +++ b/src/os/unix/ngx_freebsd_config.h @@ -85,6 +85,7 @@ #if (NGX_HAVE_KQUEUE) #include +#define NGX_KQUEUE_UDATA_T (void *) #endif diff --git a/src/os/unix/ngx_posix_config.h b/src/os/unix/ngx_posix_config.h --- a/src/os/unix/ngx_posix_config.h +++ b/src/os/unix/ngx_posix_config.h @@ -124,7 +124,17 @@ #if (NGX_HAVE_KQUEUE) + #include + +/* NetBSD before 10.0 incompatibly defines kevent.udata as "intptr_t" */ + +#if (defined __NetBSD__ && __NetBSD_Version__ < 999001500) +#define NGX_KQUEUE_UDATA_T +#else +#define NGX_KQUEUE_UDATA_T (void *) +#endif + #endif From mdounin at mdounin.ru Sun May 5 20:56:58 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sun, 05 May 2024 23:56:58 +0300 Subject: [nginx-tests] Tests: fixed auth_basic.t on OpenBSD. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/f1ba89f735ba branches: changeset: 1962:f1ba89f735ba user: Maxim Dounin date: Sun May 05 23:56:07 2024 +0300 description: Tests: fixed auth_basic.t on OpenBSD. OpenBSD does not provide any crypt() schemes except bcrypt-based "$2" anymore. As such, relevant tests are now skipped not for win32 only, but based on crypt() results. Note that just testing crypt('password', 'salt') is not enough, since Perl on win32 provides its own crypt() implementation, which is able to handle traditional DES crypt(), but rejects "$1$". diffstat: auth_basic.t | 4 +++- 1 files changed, 3 insertions(+), 1 deletions(-) diffs (14 lines): diff --git a/auth_basic.t b/auth_basic.t --- a/auth_basic.t +++ b/auth_basic.t @@ -86,7 +86,9 @@ like(http_get('/'), qr!401 Unauthorized! SKIP: { -skip 'no crypt on win32', 5 if $^O eq 'MSWin32'; +skip 'no crypt', 5 + if not crypt('password', 'salt') + or not crypt('password', '$1$salt$'); like(http_get_auth('/', 'crypt', 'password'), qr!SEETHIS!, 'normal crypt'); unlike(http_get_auth('/', 'crypt', '123'), qr!SEETHIS!, 'normal wrong'); From mdounin at mdounin.ru Sun May 5 20:56:59 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sun, 05 May 2024 23:56:59 +0300 Subject: [nginx-tests] Tests: improved has_daemon() to drop "which" stderr. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/5656138f2e46 branches: changeset: 1963:5656138f2e46 user: Maxim Dounin date: Sun May 05 23:56:16 2024 +0300 description: Tests: improved has_daemon() to drop "which" stderr. On some platforms (notably OpenBSD) "which" prints an error if the command is not found. It is now dropped to produce less clutter during testing. diffstat: lib/Test/Nginx.pm | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) diffs (12 lines): diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -336,7 +336,7 @@ sub has_daemon($) { } Test::More::plan(skip_all => "$daemon not found") - unless `which $daemon`; + unless `which $daemon 2>/dev/null`; return $self; } From mdounin at mdounin.ru Sun May 5 21:02:54 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:02:54 +0300 Subject: [nginx-tests] Tests: fixed hangs with Net::SSLeay with LibreSSL ... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/3cae7b54841e branches: changeset: 1964:3cae7b54841e user: Maxim Dounin date: Mon May 06 00:01:33 2024 +0300 description: Tests: fixed hangs with Net::SSLeay with LibreSSL on macOS. On macOS (as seen on 14.4.1, latest at the moment), Net::SSLeay compiled with LibreSSL is shipped with the OS (Net::SSLeay 1.88, LibreSSL 3.3.6). And for some reason mail_ssl_conf_command.t and stream_ssl_conf_command.t tests hang after the test, waiting indefinitely in reused SSL socket close(). This seems to be an LibreSSL bug in SSL_shutdown() (at least a couple was fixed in LibreSSL 3.5.0), probably related to session reuse (which do work in these particular tests due to TLSv1.2 explicitly used for testing). Still, calling close(SSL_no_shutdown => 1) explicitly is a trivial workaround, and hence it is implemented. diffstat: mail_ssl_conf_command.t | 2 ++ stream_ssl_conf_command.t | 2 ++ 2 files changed, 4 insertions(+), 0 deletions(-) diffs (24 lines): diff --git a/mail_ssl_conf_command.t b/mail_ssl_conf_command.t --- a/mail_ssl_conf_command.t +++ b/mail_ssl_conf_command.t @@ -103,6 +103,8 @@ like($s->socket()->dump_peer_certificate ); ok($s->socket()->get_session_reused(), 'SessionTicket'); +$s->socket()->close(SSL_no_shutdown => 1); + $s = Test::Nginx::IMAP->new( SSL => 1, SSL_cipher_list => diff --git a/stream_ssl_conf_command.t b/stream_ssl_conf_command.t --- a/stream_ssl_conf_command.t +++ b/stream_ssl_conf_command.t @@ -104,6 +104,8 @@ like($s->socket()->dump_peer_certificate ); ok($s->socket()->get_session_reused(), 'SessionTicket'); +$s->socket()->close(SSL_no_shutdown => 1); + $s = stream( PeerAddr => '127.0.0.1:' . port(8443), SSL => 1, From mdounin at mdounin.ru Sun May 5 21:02:55 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:02:55 +0300 Subject: [nginx-tests] Tests: relaxed mail_imap_ssl.t cipher matching. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/84f4d4930835 branches: changeset: 1965:84f4d4930835 user: Maxim Dounin date: Mon May 06 00:01:40 2024 +0300 description: Tests: relaxed mail_imap_ssl.t cipher matching. Previously, exact match between cipher name in the log and the one from IO::Socket:SSL was needed, which might not be the case if nginx and Net::SSLeay are compiled with different SSL libraries, notably LibreSSL (which uses names like AEAD-AES256-GCM-SHA384 till 3.5.0), and OpenSSL or BoringSSL (which use TLS_AES_256_GCM_SHA384). In particular, this affects macOS, where Net::SSLeay compiled with LibreSSL 3.3.6 is shipped with the OS, while nginx is likely to be compiled with OpenSSL. Fix is to not require exact match but instead accept properly looking names as checked by a regular expression, similarly to how it is already tested in ssl.t and stream_ssl_variables.t. diffstat: mail_imap_ssl.t | 13 ++----------- 1 files changed, 2 insertions(+), 11 deletions(-) diffs (36 lines): diff --git a/mail_imap_ssl.t b/mail_imap_ssl.t --- a/mail_imap_ssl.t +++ b/mail_imap_ssl.t @@ -29,7 +29,7 @@ select STDOUT; $| = 1; local $SIG{PIPE} = 'IGNORE'; my $t = Test::Nginx->new() - ->has(qw/mail mail_ssl imap http rewrite socket_ssl_sslversion/) + ->has(qw/mail mail_ssl imap http rewrite socket_ssl/) ->has_daemon('openssl')->plan(13) ->write_file_expand('nginx.conf', <<'EOF'); @@ -202,15 +202,6 @@ my $s = Test::Nginx::IMAP->new(); $s->send('1 AUTHENTICATE PLAIN ' . $cred->("s5")); $s->read(); -# Auth-SSL-Protocol and Auth-SSL-Cipher headers - -my ($cipher, $sslversion); - -$s = Test::Nginx::IMAP->new(SSL => 1); -$cipher = $s->socket()->get_cipher(); -$sslversion = $s->socket()->get_sslversion(); -$sslversion =~ s/_/./; - undef $s; # test auth_http request header fields with access_log @@ -229,6 +220,6 @@ like($f, qr!^on:SUCCESS:(/?CN=3.example. 'log - trusted cert'); $f = $t->read_file('auth2.log'); -like($f, qr|^$cipher:$sslversion$|m, 'log - cipher sslversion'); +like($f, qr/^[\w-]+:(TLS|SSL)v[\d.]+$/m, 'log - cipher sslversion'); ############################################################################### From mdounin at mdounin.ru Sun May 5 21:02:55 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:02:55 +0300 Subject: [nginx-tests] Tests: session reuse handling with Net::SSLeay wit... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/c924ae8d7104 branches: changeset: 1966:c924ae8d7104 user: Maxim Dounin date: Mon May 06 00:01:46 2024 +0300 description: Tests: session reuse handling with Net::SSLeay with LibreSSL. LibreSSL does not support session reuse with TLSv1.3, and this causes various test failures if Net::SSLeay is compiled with LibreSSL. Notably, this affects macOS, where Net::SSLeay compiled with LibreSSL is shipped with the OS. Fix is to mark relevant tests as TODO if Net::SSLeay is compiled with LibreSSL, similarly to what we already do for LibreSSL on the server side. diffstat: mail_ssl_session_reuse.t | 2 ++ ssl.t | 2 ++ ssl_certificate.t | 2 ++ ssl_ocsp.t | 4 ++++ ssl_session_reuse.t | 2 ++ ssl_session_ticket_key.t | 2 ++ ssl_sni.t | 2 ++ ssl_sni_sessions.t | 2 ++ stream_ssl_certificate.t | 2 ++ stream_ssl_session_reuse.t | 2 ++ stream_ssl_variables.t | 2 ++ 11 files changed, 24 insertions(+), 0 deletions(-) diffs (141 lines): diff --git a/mail_ssl_session_reuse.t b/mail_ssl_session_reuse.t --- a/mail_ssl_session_reuse.t +++ b/mail_ssl_session_reuse.t @@ -140,6 +140,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); is(test_reuse(8993), 1, 'tickets reused'); is(test_reuse(8994), 1, 'tickets and cache reused'); diff --git a/ssl.t b/ssl.t --- a/ssl.t +++ b/ssl.t @@ -186,6 +186,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); like(get('/', 8085, $ctx), qr/^body r$/m, 'session reused'); diff --git a/ssl_certificate.t b/ssl_certificate.t --- a/ssl_certificate.t +++ b/ssl_certificate.t @@ -171,6 +171,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $Net::SSLeay::VERSION < 1.88 && test_tls13(); local $TODO = 'no TLSv1.3 sessions, old IO::Socket::SSL' if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); +local $TODO = 'not TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); like(get('default', 8080, $s), qr/default:r/, 'session reused'); diff --git a/ssl_ocsp.t b/ssl_ocsp.t --- a/ssl_ocsp.t +++ b/ssl_ocsp.t @@ -361,6 +361,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); like(get('ec-end', ses => $s), qr/200 OK.*SUCCESS:r/s, 'session reused'); @@ -393,6 +395,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); like(get('ec-end', ses => $s), qr/400 Bad.*FAILED:certificate revoked:r/s, 'session reused - revoked'); diff --git a/ssl_session_reuse.t b/ssl_session_reuse.t --- a/ssl_session_reuse.t +++ b/ssl_session_reuse.t @@ -167,6 +167,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); is(test_reuse(8443), 1, 'tickets reused'); is(test_reuse(8444), 1, 'tickets and cache reused'); diff --git a/ssl_session_ticket_key.t b/ssl_session_ticket_key.t --- a/ssl_session_ticket_key.t +++ b/ssl_session_ticket_key.t @@ -105,6 +105,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); cmp_ok(get_ticket_key_name(), 'ne', $key, 'ticket key next'); diff --git a/ssl_sni.t b/ssl_sni.t --- a/ssl_sni.t +++ b/ssl_sni.t @@ -128,6 +128,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); like(get('/name', 'localhost', $ctx), qr/^r:localhost$/m, 'ssl server name - reused'); diff --git a/ssl_sni_sessions.t b/ssl_sni_sessions.t --- a/ssl_sni_sessions.t +++ b/ssl_sni_sessions.t @@ -116,6 +116,8 @@ plan(skip_all => 'no TLSv1.3 sessions, o if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); plan(skip_all => 'no TLSv1.3 sessions in LibreSSL') if $t->has_module('LibreSSL') && test_tls13(); +plan(skip_all => 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)') + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); plan(skip_all => 'no TLS 1.3 session cache in BoringSSL') if $t->has_module('BoringSSL') && test_tls13(); diff --git a/stream_ssl_certificate.t b/stream_ssl_certificate.t --- a/stream_ssl_certificate.t +++ b/stream_ssl_certificate.t @@ -148,6 +148,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $Net::SSLeay::VERSION < 1.88 && test_tls13(); local $TODO = 'no TLSv1.3 sessions, old IO::Socket::SSL' if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); like(get('default', 8080, $s), qr/default:r/, 'session reused'); diff --git a/stream_ssl_session_reuse.t b/stream_ssl_session_reuse.t --- a/stream_ssl_session_reuse.t +++ b/stream_ssl_session_reuse.t @@ -144,6 +144,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); is(test_reuse(8443), 1, 'tickets reused'); is(test_reuse(8444), 1, 'tickets and cache reused'); diff --git a/stream_ssl_variables.t b/stream_ssl_variables.t --- a/stream_ssl_variables.t +++ b/stream_ssl_variables.t @@ -98,6 +98,8 @@ local $TODO = 'no TLSv1.3 sessions, old if $IO::Socket::SSL::VERSION < 2.061 && test_tls13(); local $TODO = 'no TLSv1.3 sessions in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); $s = stream( PeerAddr => '127.0.0.1:' . port(8443), From mdounin at mdounin.ru Sun May 5 21:02:55 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:02:55 +0300 Subject: [nginx-tests] Tests: sigalgs handling with Net::SSLeay with Libr... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/0a93f101925a branches: changeset: 1967:0a93f101925a user: Maxim Dounin date: Mon May 06 00:01:53 2024 +0300 description: Tests: sigalgs handling with Net::SSLeay with LibreSSL. LibreSSL does not provide a way to configure signature algorithms, and this makes it impossible to request a particular server certificate when using TLSv1.3. As such, relevant tests fail if Net::SSLeay is compiled with LibreSSL. Notably, this affects macOS, where Net::SSLeay compiled with LibreSSL is shipped with the OS. Fix is to mark relevant tests as TODO if Net:SSLeay is compiled with LibreSSL, similarly to what we already do for sigalg issues in LibreSSL on the server side. diffstat: ssl_certificates.t | 8 ++++++++ ssl_stapling.t | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletions(-) diffs (52 lines): diff --git a/ssl_certificates.t b/ssl_certificates.t --- a/ssl_certificates.t +++ b/ssl_certificates.t @@ -99,8 +99,16 @@ like(cert('RSA'), qr/CN=rsa/, 'ssl cert } +TODO: { +local $TODO = 'no TLSv1.3 sigalgs in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") + && !$t->has_module('LibreSSL') + && test_tls13(); + like(cert('ECDSA'), qr/CN=ec/, 'ssl cert ECDSA'); +} + ############################################################################### sub test_tls13 { diff --git a/ssl_stapling.t b/ssl_stapling.t --- a/ssl_stapling.t +++ b/ssl_stapling.t @@ -262,6 +262,8 @@ ok(!staple(8443, 'RSA'), 'staple revoked TODO: { local $TODO = 'broken TLSv1.3 sigalgs in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sigalgs in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); ok(staple(8443, 'ECDSA'), 'staple success'); @@ -272,6 +274,8 @@ ok(!staple(8444, 'RSA'), 'responder revo TODO: { local $TODO = 'broken TLSv1.3 sigalgs in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 sigalgs in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); ok(staple(8444, 'ECDSA'), 'responder success'); @@ -288,7 +292,9 @@ ok(!staple(8449, 'ECDSA'), 'ocsp error') TODO: { local $TODO = 'broken TLSv1.3 sigalgs in LibreSSL' - if $t->has_module('LibreSSL') && test_tls13(); + if $t->has_module('LibreSSL') + && !Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") + && test_tls13(); like(`grep -F '[crit]' ${\($t->testdir())}/error.log`, qr/^$/s, 'no crit'); From mdounin at mdounin.ru Sun May 5 21:02:55 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:02:55 +0300 Subject: [nginx-tests] Tests: CA list handling with Net::SSLeay with Libr... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/b72a8c4a1bef branches: changeset: 1968:b72a8c4a1bef user: Maxim Dounin date: Mon May 06 00:02:01 2024 +0300 description: Tests: CA list handling with Net::SSLeay with LibreSSL. LibreSSL is not able to work with CA lists (certificate_authorities extension) when using TLSv1.3. As such, relevant tests fail if Net::SSLeay is compiled with LibreSSL. Notably, this affects macOS, where Net::SSLeay compiled with LibreSSL is shipped with the OS. Fix is to mark relevant tests as TODO if Net:SSLeay is compiled with LibreSSL, similarly to what we already do for TLSv1.3 CA list issues in LibreSSL on the server side. diffstat: ssl_verify_client.t | 2 ++ stream_ssl_verify_client.t | 2 ++ 2 files changed, 4 insertions(+), 0 deletions(-) diffs (24 lines): diff --git a/ssl_verify_client.t b/ssl_verify_client.t --- a/ssl_verify_client.t +++ b/ssl_verify_client.t @@ -158,6 +158,8 @@ skip 'Net::SSLeay version >= 1.36 requir TODO: { local $TODO = 'broken TLSv1.3 CA list in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 CA list in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); my $ca = join ' ', get('optional', '3.example.com'); is($ca, '/CN=2.example.com', 'no trusted sent'); diff --git a/stream_ssl_verify_client.t b/stream_ssl_verify_client.t --- a/stream_ssl_verify_client.t +++ b/stream_ssl_verify_client.t @@ -126,6 +126,8 @@ skip 'Net::SSLeay version >= 1.36 requir TODO: { local $TODO = 'broken TLSv1.3 CA list in LibreSSL' if $t->has_module('LibreSSL') && test_tls13(); +local $TODO = 'no TLSv1.3 CA list in Net::SSLeay (LibreSSL)' + if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); my $ca = join ' ', get(8082, '3.example.com'); is($ca, '/CN=2.example.com', 'no trusted sent'); From mdounin at mdounin.ru Sun May 5 21:05:01 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:05:01 +0300 Subject: [nginx-tests] Tests: adjusted stream_ssl_alpn.t to handle old SS... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/3ba1668cea87 branches: changeset: 1969:3ba1668cea87 user: Maxim Dounin date: Mon May 06 00:03:01 2024 +0300 description: Tests: adjusted stream_ssl_alpn.t to handle old SSL libraries. The "ssl_alpn" directive of the stream module requires an SSL library with ALPN support, and startup fails if the directive is used in the configuration but ALPN support is not available (notably, with OpenSSL before 1.0.2). Restored try_run() to skip the test in this case. diffstat: stream_ssl_alpn.t | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) diffs (12 lines): diff --git a/stream_ssl_alpn.t b/stream_ssl_alpn.t --- a/stream_ssl_alpn.t +++ b/stream_ssl_alpn.t @@ -71,7 +71,7 @@ foreach my $name ('localhost') { or die "Can't create certificate for $name: $!\n"; } -$t->run()->plan(6); +$t->try_run('no ssl_alpn')->plan(6); ############################################################################### From mdounin at mdounin.ru Sun May 5 21:05:01 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:05:01 +0300 Subject: [nginx-tests] Tests: relaxed SSL version used in testing. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/6d3a8f4eb9b2 branches: changeset: 1970:6d3a8f4eb9b2 user: Maxim Dounin date: Mon May 06 00:03:13 2024 +0300 description: Tests: relaxed SSL version used in testing. This ensures that tests can be properly run with old OpenSSL versions when using recent versions of IO::Socket::SSL (which defaults to TLS 1.2+ starting with IO::Socket:SSL version 2.082, and therefore not compatible with OpenSSL before 1.0.1). diffstat: h2_ssl.t | 1 + h2_ssl_verify_client.t | 1 + lib/Test/Nginx.pm | 1 + lib/Test/Nginx/HTTP2.pm | 1 + lib/Test/Nginx/IMAP.pm | 1 + lib/Test/Nginx/POP3.pm | 1 + lib/Test/Nginx/SMTP.pm | 1 + lib/Test/Nginx/Stream.pm | 1 + ssl.t | 1 + ssl_proxy_upgrade.t | 1 + ssl_sni.t | 1 + ssl_sni_sessions.t | 1 + stream_proxy_protocol_ssl.t | 1 + stream_ssl_realip.t | 1 + 14 files changed, 14 insertions(+), 0 deletions(-) diffs (154 lines): diff --git a/h2_ssl.t b/h2_ssl.t --- a/h2_ssl.t +++ b/h2_ssl.t @@ -154,6 +154,7 @@ sub get_ssl_socket { Proto => 'tcp', PeerAddr => '127.0.0.1', PeerPort => port(8080), + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_alpn_protocols => $alpn, SSL_error_trap => sub { die $_[1] } diff --git a/h2_ssl_verify_client.t b/h2_ssl_verify_client.t --- a/h2_ssl_verify_client.t +++ b/h2_ssl_verify_client.t @@ -112,6 +112,7 @@ sub get_ssl_socket { Proto => 'tcp', PeerAddr => '127.0.0.1', PeerPort => port(8080), + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_alpn_protocols => [ 'h2' ], SSL_hostname => $sni, diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -872,6 +872,7 @@ sub http_start($;%) { require IO::Socket::SSL; IO::Socket::SSL->start_SSL( $s, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), %extra diff --git a/lib/Test/Nginx/HTTP2.pm b/lib/Test/Nginx/HTTP2.pm --- a/lib/Test/Nginx/HTTP2.pm +++ b/lib/Test/Nginx/HTTP2.pm @@ -548,6 +548,7 @@ sub new_socket { ); require IO::Socket::SSL if $extra{'SSL'}; IO::Socket::SSL->start_SSL($s, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_npn_protocols => $npn ? [ $npn ] : undef, SSL_alpn_protocols => $alpn ? [ $alpn ] : undef, diff --git a/lib/Test/Nginx/IMAP.pm b/lib/Test/Nginx/IMAP.pm --- a/lib/Test/Nginx/IMAP.pm +++ b/lib/Test/Nginx/IMAP.pm @@ -38,6 +38,7 @@ sub new { require IO::Socket::SSL; IO::Socket::SSL->start_SSL( $self->{_socket}, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), @_ diff --git a/lib/Test/Nginx/POP3.pm b/lib/Test/Nginx/POP3.pm --- a/lib/Test/Nginx/POP3.pm +++ b/lib/Test/Nginx/POP3.pm @@ -38,6 +38,7 @@ sub new { require IO::Socket::SSL; IO::Socket::SSL->start_SSL( $self->{_socket}, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), @_ diff --git a/lib/Test/Nginx/SMTP.pm b/lib/Test/Nginx/SMTP.pm --- a/lib/Test/Nginx/SMTP.pm +++ b/lib/Test/Nginx/SMTP.pm @@ -38,6 +38,7 @@ sub new { require IO::Socket::SSL; IO::Socket::SSL->start_SSL( $self->{_socket}, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), @_ diff --git a/lib/Test/Nginx/Stream.pm b/lib/Test/Nginx/Stream.pm --- a/lib/Test/Nginx/Stream.pm +++ b/lib/Test/Nginx/Stream.pm @@ -54,6 +54,7 @@ sub new { require IO::Socket::SSL; IO::Socket::SSL->start_SSL( $self->{_socket}, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), @_ diff --git a/ssl.t b/ssl.t --- a/ssl.t +++ b/ssl.t @@ -315,6 +315,7 @@ sub cert { sub get_ssl_context { return IO::Socket::SSL::SSL_Context->new( + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_session_cache_size => 100 ); diff --git a/ssl_proxy_upgrade.t b/ssl_proxy_upgrade.t --- a/ssl_proxy_upgrade.t +++ b/ssl_proxy_upgrade.t @@ -170,6 +170,7 @@ sub upgrade_connect { my $s = IO::Socket::SSL->new( Proto => 'tcp', PeerAddr => '127.0.0.1:' . port(8080), + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), ) or die "Can't connect to nginx: $!\n"; diff --git a/ssl_sni.t b/ssl_sni.t --- a/ssl_sni.t +++ b/ssl_sni.t @@ -116,6 +116,7 @@ like(get_host('example.org', 'example.co # $ssl_server_name in sessions my $ctx = new IO::Socket::SSL::SSL_Context( + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_session_cache_size => 100); diff --git a/ssl_sni_sessions.t b/ssl_sni_sessions.t --- a/ssl_sni_sessions.t +++ b/ssl_sni_sessions.t @@ -159,6 +159,7 @@ like(get('tickets', 8444, $ctx), qr!tick sub get_ssl_context { return IO::Socket::SSL::SSL_Context->new( + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_session_cache_size => 100 ); diff --git a/stream_proxy_protocol_ssl.t b/stream_proxy_protocol_ssl.t --- a/stream_proxy_protocol_ssl.t +++ b/stream_proxy_protocol_ssl.t @@ -152,6 +152,7 @@ sub stream_daemon_ssl { eval { IO::Socket::SSL->start_SSL($client, SSL_server => 1, + SSL_version => 'SSLv23', SSL_cert_file => "$d/localhost.crt", SSL_key_file => "$d/localhost.key", SSL_error_trap => sub { die $_[1] } diff --git a/stream_ssl_realip.t b/stream_ssl_realip.t --- a/stream_ssl_realip.t +++ b/stream_ssl_realip.t @@ -133,6 +133,7 @@ sub pp_get { local $SIG{PIPE} = sub { die "sigpipe\n" }; alarm(8); IO::Socket::SSL->start_SSL($s, + SSL_version => 'SSLv23', SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_error_trap => sub { die $_[1] } ); From mdounin at mdounin.ru Sun May 5 21:05:01 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:05:01 +0300 Subject: [nginx-tests] Tests: guarded session ticket tests for old OpenSS... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/ab45ee8011df branches: changeset: 1971:ab45ee8011df user: Maxim Dounin date: Mon May 06 00:03:16 2024 +0300 description: Tests: guarded session ticket tests for old OpenSSL versions. Much like SNI support, TLS session tickets are available starting with OpenSSL 0.9.8f if TLS extensions support is explicitly configured, and enabled by default since 0.9.8j. As such, SNI availability is checked to ensure TLS extensions support is compiled in. Additionally, the ssl_session_ticket_key.t tests for automatic ticket key rotation, which uses session ticket key callback, as introduced in OpenSSL 0.9.8h. diffstat: lib/Test/Nginx.pm | 1 + mail_ssl_session_reuse.t | 6 ++++++ ssl_session_reuse.t | 6 ++++++ ssl_session_ticket_key.t | 4 +++- stream_ssl_session_reuse.t | 6 ++++++ 5 files changed, 22 insertions(+), 1 deletions(-) diffs (83 lines): diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -114,6 +114,7 @@ sub has_module($) { my %regex = ( sni => 'TLS SNI support enabled', + tickets => 'TLS SNI support enabled', mail => '--with-mail((?!\S)|=dynamic)', flv => '--with-http_flv_module', perl => '--with-http_perl_module', diff --git a/mail_ssl_session_reuse.t b/mail_ssl_session_reuse.t --- a/mail_ssl_session_reuse.t +++ b/mail_ssl_session_reuse.t @@ -143,7 +143,13 @@ local $TODO = 'no TLSv1.3 sessions in Li local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); +TODO: { +local $TODO = 'no session tickets' unless $t->has_module('tickets'); + is(test_reuse(8993), 1, 'tickets reused'); + +} + is(test_reuse(8994), 1, 'tickets and cache reused'); TODO: { diff --git a/ssl_session_reuse.t b/ssl_session_reuse.t --- a/ssl_session_reuse.t +++ b/ssl_session_reuse.t @@ -170,7 +170,13 @@ local $TODO = 'no TLSv1.3 sessions in Li local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); +TODO: { +local $TODO = 'no session tickets' unless $t->has_module('tickets'); + is(test_reuse(8443), 1, 'tickets reused'); + +} + is(test_reuse(8444), 1, 'tickets and cache reused'); TODO: { diff --git a/ssl_session_ticket_key.t b/ssl_session_ticket_key.t --- a/ssl_session_ticket_key.t +++ b/ssl_session_ticket_key.t @@ -27,7 +27,7 @@ plan(skip_all => 'Net::SSLeay version => eval { require IO::Socket::SSL; die if $IO::Socket::SSL::VERSION < 2.030; }; plan(skip_all => 'IO::Socket::SSL version => 2.030 required') if $@; -my $t = Test::Nginx->new()->has(qw/http http_ssl socket_ssl/) +my $t = Test::Nginx->new()->has(qw/http http_ssl tickets socket_ssl/) ->has_daemon('openssl')->plan(2) ->write_file_expand('nginx.conf', <<'EOF'); @@ -99,6 +99,8 @@ is(get_ticket_key_name(), $key, 'ticket select undef, undef, undef, 2.5; +local $TODO = 'no ticket key callback' + if $t->has_module('OpenSSL') and not $t->has_feature('openssl:0.9.8h'); local $TODO = 'no TLSv1.3 sessions, old Net::SSLeay' if $Net::SSLeay::VERSION < 1.88 && test_tls13(); local $TODO = 'no TLSv1.3 sessions, old IO::Socket::SSL' diff --git a/stream_ssl_session_reuse.t b/stream_ssl_session_reuse.t --- a/stream_ssl_session_reuse.t +++ b/stream_ssl_session_reuse.t @@ -147,7 +147,13 @@ local $TODO = 'no TLSv1.3 sessions in Li local $TODO = 'no TLSv1.3 sessions in Net::SSLeay (LibreSSL)' if Net::SSLeay::constant("LIBRESSL_VERSION_NUMBER") && test_tls13(); +TODO: { +local $TODO = 'no session tickets' unless $t->has_module('tickets'); + is(test_reuse(8443), 1, 'tickets reused'); + +} + is(test_reuse(8444), 1, 'tickets and cache reused'); TODO: { From mdounin at mdounin.ru Sun May 5 21:05:01 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:05:01 +0300 Subject: [nginx-tests] Tests: guarded ssl_stapling.t for old OpenSSL vers... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/f3573393f36f branches: changeset: 1972:f3573393f36f user: Maxim Dounin date: Mon May 06 00:04:22 2024 +0300 description: Tests: guarded ssl_stapling.t for old OpenSSL versions. Stapling requires the certificate status TLS extension. It is implemented in OpenSSL 0.9.8h and available if TLS extensions support is explicitly configured, and by default since 0.9.8j. To properly test if stapling is available, OpenSSL version is checked, and SNI availability is checked to ensure TLS extensions support is compiled in. diffstat: ssl_stapling.t | 7 ++++++- 1 files changed, 6 insertions(+), 1 deletions(-) diffs (17 lines): diff --git a/ssl_stapling.t b/ssl_stapling.t --- a/ssl_stapling.t +++ b/ssl_stapling.t @@ -32,7 +32,12 @@ plan(skip_all => 'Net::SSLeay too old') eval { defined &IO::Socket::SSL::SSL_OCSP_TRY_STAPLE or die; }; plan(skip_all => 'IO::Socket::SSL too old') if $@; -plan(skip_all => 'no OCSP stapling') if $t->has_module('BoringSSL'); +plan(skip_all => 'no OCSP stapling') + if $t->has_module('BoringSSL'); +plan(skip_all => 'no OCSP stapling') + if $t->has_module('OpenSSL') and not $t->has_feature('openssl:0.9.8h'); +plan(skip_all => 'no OCSP stapling') + if not $t->has_module('sni'); $t->plan(10)->write_file_expand('nginx.conf', <<'EOF'); From mdounin at mdounin.ru Sun May 5 21:05:01 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:05:01 +0300 Subject: [nginx-tests] Tests: guarded ssl_certificates.t for old OpenSSL ... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/94e0390dc64f branches: changeset: 1973:94e0390dc64f user: Maxim Dounin date: Mon May 06 00:04:24 2024 +0300 description: Tests: guarded ssl_certificates.t for old OpenSSL versions. The ssl_certificates.t test uses ECDSA ciphers, which are only available starting with OpenSSL 0.9.8b. In previous versions, notably in OpenSSL 0.9.8 and 0.9.8a, only non-standard example ciphers where available, which cannot be used with any other clients. The same applies to ssl_stapling.t, but it also requires the certificate status TLS extension, which is only available since OpenSSL 0.9.8h, and therefore no additional checks are needed. diffstat: ssl_certificates.t | 5 ++++- 1 files changed, 4 insertions(+), 1 deletions(-) diffs (15 lines): diff --git a/ssl_certificates.t b/ssl_certificates.t --- a/ssl_certificates.t +++ b/ssl_certificates.t @@ -25,7 +25,10 @@ select STDOUT; $| = 1; my $t = Test::Nginx->new()->has(qw/http http_ssl socket_ssl/) ->has_daemon('openssl'); -plan(skip_all => 'no multiple certificates') if $t->has_module('BoringSSL'); +plan(skip_all => 'no multiple certificates') + if $t->has_module('BoringSSL'); +plan(skip_all => 'no ECDSA support') + if $t->has_module('OpenSSL') and not $t->has_feature('openssl:0.9.8b'); $t->write_file_expand('nginx.conf', <<'EOF'); From mdounin at mdounin.ru Sun May 5 21:05:01 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:05:01 +0300 Subject: [nginx-tests] Tests: improved compatibility when using recent "o... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/b5036a0f9ae0 branches: changeset: 1974:b5036a0f9ae0 user: Maxim Dounin date: Mon May 06 00:04:26 2024 +0300 description: Tests: improved compatibility when using recent "openssl" app. Starting with OpenSSL 3.0, "openssl genrsa" generates encrypted keys in PKCS#8 format instead of previously used PKCS#1 format. Further, since OpenSSL 1.1.0 such keys are using PBKDF2 hmacWithSHA256. Such keys are not supported by old SSL libraries, notably by OpenSSL before 1.0.0 (OpenSSL 0.9.8 only supports hmacWithSHA1) and by BoringSSL before May 21, 2019 (support for hmacWithSHA256 was added in 302a4dee6c), and trying to load such keys into nginx compiled with an old SSL library results in "unsupported prf" errors. To facilitate testing with old SSL libraries, keys are now generated with "openssl genrsa -traditional" if the flag is available. diffstat: grpc_ssl.t | 3 ++- mail_ssl.t | 3 ++- proxy_ssl_certificate.t | 3 ++- proxy_ssl_certificate_vars.t | 3 ++- ssl_certificate.t | 3 ++- ssl_password_file.t | 4 +++- stream_proxy_ssl_certificate.t | 3 ++- stream_proxy_ssl_certificate_vars.t | 3 ++- stream_ssl.t | 4 +++- stream_ssl_certificate.t | 3 ++- uwsgi_ssl_certificate.t | 3 ++- uwsgi_ssl_certificate_vars.t | 3 ++- 12 files changed, 26 insertions(+), 12 deletions(-) diffs (232 lines): diff --git a/grpc_ssl.t b/grpc_ssl.t --- a/grpc_ssl.t +++ b/grpc_ssl.t @@ -103,6 +103,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('localhost') { system('openssl req -x509 -new ' @@ -114,7 +115,7 @@ foreach my $name ('localhost') { foreach my $name ('client') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/mail_ssl.t b/mail_ssl.t --- a/mail_ssl.t +++ b/mail_ssl.t @@ -115,10 +115,11 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('localhost', 'inherits') { system("openssl genrsa -out $d/$name.key -passout pass:localhost " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/proxy_ssl_certificate.t b/proxy_ssl_certificate.t --- a/proxy_ssl_certificate.t +++ b/proxy_ssl_certificate.t @@ -107,6 +107,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('1.example.com', '2.example.com') { system('openssl req -x509 -new ' @@ -118,7 +119,7 @@ foreach my $name ('1.example.com', '2.ex foreach my $name ('3.example.com') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/proxy_ssl_certificate_vars.t b/proxy_ssl_certificate_vars.t --- a/proxy_ssl_certificate_vars.t +++ b/proxy_ssl_certificate_vars.t @@ -106,6 +106,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('1.example.com', '2.example.com') { system('openssl req -x509 -new ' @@ -117,7 +118,7 @@ foreach my $name ('1.example.com', '2.ex foreach my $name ('3.example.com') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/ssl_certificate.t b/ssl_certificate.t --- a/ssl_certificate.t +++ b/ssl_certificate.t @@ -125,6 +125,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('one', 'two') { system('openssl req -x509 -new ' @@ -136,7 +137,7 @@ foreach my $name ('one', 'two') { foreach my $name ('pass') { system("openssl genrsa -out $d/$name.key -passout pass:pass " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create $name key: $!\n"; system("openssl req -x509 -new -config $d/openssl.conf " . "-subj /CN=$name/ -out $d/$name.crt -key $d/$name.key " diff --git a/ssl_password_file.t b/ssl_password_file.t --- a/ssl_password_file.t +++ b/ssl_password_file.t @@ -94,11 +94,13 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; + mkfifo("$d/password_fifo", 0700); foreach my $name ('localhost', 'inherits') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/stream_proxy_ssl_certificate.t b/stream_proxy_ssl_certificate.t --- a/stream_proxy_ssl_certificate.t +++ b/stream_proxy_ssl_certificate.t @@ -113,6 +113,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('1.example.com', '2.example.com') { system('openssl req -x509 -new ' @@ -124,7 +125,7 @@ foreach my $name ('1.example.com', '2.ex foreach my $name ('3.example.com') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/stream_proxy_ssl_certificate_vars.t b/stream_proxy_ssl_certificate_vars.t --- a/stream_proxy_ssl_certificate_vars.t +++ b/stream_proxy_ssl_certificate_vars.t @@ -120,6 +120,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('1.example.com', '2.example.com') { system('openssl req -x509 -new ' @@ -131,7 +132,7 @@ foreach my $name ('1.example.com', '2.ex foreach my $name ('3.example.com') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/stream_ssl.t b/stream_ssl.t --- a/stream_ssl.t +++ b/stream_ssl.t @@ -90,11 +90,13 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; + mkfifo("$d/password_fifo", 0700); foreach my $name ('localhost', 'inherits') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/stream_ssl_certificate.t b/stream_ssl_certificate.t --- a/stream_ssl_certificate.t +++ b/stream_ssl_certificate.t @@ -108,6 +108,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('one', 'two') { system('openssl req -x509 -new ' @@ -119,7 +120,7 @@ foreach my $name ('one', 'two') { foreach my $name ('pass') { system("openssl genrsa -out $d/$name.key -passout pass:pass " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create $name key: $!\n"; system("openssl req -x509 -new -config $d/openssl.conf " . "-subj /CN=$name/ -out $d/$name.crt -key $d/$name.key " diff --git a/uwsgi_ssl_certificate.t b/uwsgi_ssl_certificate.t --- a/uwsgi_ssl_certificate.t +++ b/uwsgi_ssl_certificate.t @@ -105,6 +105,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('1.example.com', '2.example.com') { system('openssl req -x509 -new ' @@ -116,7 +117,7 @@ foreach my $name ('1.example.com', '2.ex foreach my $name ('3.example.com') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " diff --git a/uwsgi_ssl_certificate_vars.t b/uwsgi_ssl_certificate_vars.t --- a/uwsgi_ssl_certificate_vars.t +++ b/uwsgi_ssl_certificate_vars.t @@ -104,6 +104,7 @@ distinguished_name = req_distinguished_n EOF my $d = $t->testdir(); +my $tr = `openssl genrsa -help 2>&1` =~ /-traditional/ ? '-traditional' : ''; foreach my $name ('1.example.com', '2.example.com') { system('openssl req -x509 -new ' @@ -115,7 +116,7 @@ foreach my $name ('1.example.com', '2.ex foreach my $name ('3.example.com') { system("openssl genrsa -out $d/$name.key -passout pass:$name " - . "-aes128 2048 >>$d/openssl.out 2>&1") == 0 + . "-aes128 $tr 2048 >>$d/openssl.out 2>&1") == 0 or die "Can't create private key: $!\n"; system('openssl req -x509 -new ' . "-config $d/openssl.conf -subj /CN=$name/ " From mdounin at mdounin.ru Sun May 5 21:12:38 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:12:38 +0300 Subject: [nginx] SSL: logging level of all "SSL alert number N" errors. Message-ID: details: http://freenginx.org/hg/nginx/rev/f5423ee155fe branches: changeset: 9264:f5423ee155fe user: Maxim Dounin date: Mon May 06 00:06:15 2024 +0300 description: SSL: logging level of all "SSL alert number N" errors. Errors about alerts received from peers are generated by OpenSSL by adding peer-provided alert description (from 0 to 255) to SSL_AD_REASON_OFFSET. All such errors, including ones for unknown alerts, are now logged at the "info" level, as these can be caused by a misbehaving client. diffstat: src/event/ngx_event_openssl.c | 28 ++-------------------------- 1 files changed, 2 insertions(+), 26 deletions(-) diffs (38 lines): diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -3590,32 +3590,8 @@ ngx_ssl_connection_error(ngx_connection_ #ifdef SSL_R_BAD_RECORD_TYPE || n == SSL_R_BAD_RECORD_TYPE /* 443 */ #endif - || n == 1000 /* SSL_R_SSLV3_ALERT_CLOSE_NOTIFY */ -#ifdef SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE - || n == SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE /* 1010 */ - || n == SSL_R_SSLV3_ALERT_BAD_RECORD_MAC /* 1020 */ - || n == SSL_R_TLSV1_ALERT_DECRYPTION_FAILED /* 1021 */ - || n == SSL_R_TLSV1_ALERT_RECORD_OVERFLOW /* 1022 */ - || n == SSL_R_SSLV3_ALERT_DECOMPRESSION_FAILURE /* 1030 */ - || n == SSL_R_SSLV3_ALERT_HANDSHAKE_FAILURE /* 1040 */ - || n == SSL_R_SSLV3_ALERT_NO_CERTIFICATE /* 1041 */ - || n == SSL_R_SSLV3_ALERT_BAD_CERTIFICATE /* 1042 */ - || n == SSL_R_SSLV3_ALERT_UNSUPPORTED_CERTIFICATE /* 1043 */ - || n == SSL_R_SSLV3_ALERT_CERTIFICATE_REVOKED /* 1044 */ - || n == SSL_R_SSLV3_ALERT_CERTIFICATE_EXPIRED /* 1045 */ - || n == SSL_R_SSLV3_ALERT_CERTIFICATE_UNKNOWN /* 1046 */ - || n == SSL_R_SSLV3_ALERT_ILLEGAL_PARAMETER /* 1047 */ - || n == SSL_R_TLSV1_ALERT_UNKNOWN_CA /* 1048 */ - || n == SSL_R_TLSV1_ALERT_ACCESS_DENIED /* 1049 */ - || n == SSL_R_TLSV1_ALERT_DECODE_ERROR /* 1050 */ - || n == SSL_R_TLSV1_ALERT_DECRYPT_ERROR /* 1051 */ - || n == SSL_R_TLSV1_ALERT_EXPORT_RESTRICTION /* 1060 */ - || n == SSL_R_TLSV1_ALERT_PROTOCOL_VERSION /* 1070 */ - || n == SSL_R_TLSV1_ALERT_INSUFFICIENT_SECURITY /* 1071 */ - || n == SSL_R_TLSV1_ALERT_INTERNAL_ERROR /* 1080 */ - || n == SSL_R_TLSV1_ALERT_USER_CANCELLED /* 1090 */ - || n == SSL_R_TLSV1_ALERT_NO_RENEGOTIATION /* 1100 */ -#endif + || (n >= SSL_AD_REASON_OFFSET /* 1000 */ + && n <= SSL_AD_REASON_OFFSET + 255) ) { switch (c->log_error) { From mdounin at mdounin.ru Sun May 5 21:12:38 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:12:38 +0300 Subject: [nginx] SSL: logging level of "invalid alert" errors. Message-ID: details: http://freenginx.org/hg/nginx/rev/d89e0386b695 branches: changeset: 9265:d89e0386b695 user: Maxim Dounin date: Mon May 06 00:07:18 2024 +0300 description: SSL: logging level of "invalid alert" errors. The SSL_R_INVALID_ALERT ("invalid alert") errors are reported by OpenSSL 1.1.1 or newer if the client sends a malformed alert. These errors are now logged at the "info" level. diffstat: src/event/ngx_event_openssl.c | 3 +++ 1 files changed, 3 insertions(+), 0 deletions(-) diffs (13 lines): diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c --- a/src/event/ngx_event_openssl.c +++ b/src/event/ngx_event_openssl.c @@ -3495,6 +3495,9 @@ ngx_ssl_connection_error(ngx_connection_ #ifdef SSL_R_PACKET_LENGTH_TOO_LONG || n == SSL_R_PACKET_LENGTH_TOO_LONG /* 198 */ #endif +#ifdef SSL_R_INVALID_ALERT + || n == SSL_R_INVALID_ALERT /* 205 */ +#endif || n == SSL_R_RECORD_LENGTH_MISMATCH /* 213 */ #ifdef SSL_R_TOO_MANY_WARNING_ALERTS || n == SSL_R_TOO_MANY_WARNING_ALERTS /* 220 */ From mdounin at mdounin.ru Sun May 5 21:12:38 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 06 May 2024 00:12:38 +0300 Subject: [nginx] Fixed compilation on NetBSD 10. Message-ID: details: http://freenginx.org/hg/nginx/rev/93bbb9fbf30d branches: changeset: 9266:93bbb9fbf30d user: Maxim Dounin date: Mon May 06 00:07:33 2024 +0300 description: Fixed compilation on NetBSD 10. NetBSD 10 changed struct kevent's udata type from "intptr_t" to "void *", similarly to how it is defined on other systems. This breaks compilation, since intptr_t is assumed on NetBSD. Fix is to limit special intptr_t handling to NetBSD versions before 10.0. To simplify testing, relevant definitions are moved from configure to code (which seems to be the right thing anyway). diffstat: auto/unix | 20 -------------------- src/os/unix/ngx_darwin_config.h | 1 + src/os/unix/ngx_freebsd_config.h | 1 + src/os/unix/ngx_posix_config.h | 10 ++++++++++ 4 files changed, 12 insertions(+), 20 deletions(-) diffs (73 lines): diff --git a/auto/unix b/auto/unix --- a/auto/unix +++ b/auto/unix @@ -129,26 +129,6 @@ if test -z "$NGX_KQUEUE_CHECKED"; then fi -if [ "$NGX_SYSTEM" = "NetBSD" ]; then - - # NetBSD 2.0 incompatibly defines kevent.udata as "intptr_t" - - cat << END >> $NGX_AUTO_CONFIG_H - -#define NGX_KQUEUE_UDATA_T - -END - -else - cat << END >> $NGX_AUTO_CONFIG_H - -#define NGX_KQUEUE_UDATA_T (void *) - -END - -fi - - ngx_feature="crypt()" ngx_feature_name= ngx_feature_run=no diff --git a/src/os/unix/ngx_darwin_config.h b/src/os/unix/ngx_darwin_config.h --- a/src/os/unix/ngx_darwin_config.h +++ b/src/os/unix/ngx_darwin_config.h @@ -74,6 +74,7 @@ #if (NGX_HAVE_KQUEUE) #include +#define NGX_KQUEUE_UDATA_T (void *) #endif diff --git a/src/os/unix/ngx_freebsd_config.h b/src/os/unix/ngx_freebsd_config.h --- a/src/os/unix/ngx_freebsd_config.h +++ b/src/os/unix/ngx_freebsd_config.h @@ -85,6 +85,7 @@ #if (NGX_HAVE_KQUEUE) #include +#define NGX_KQUEUE_UDATA_T (void *) #endif diff --git a/src/os/unix/ngx_posix_config.h b/src/os/unix/ngx_posix_config.h --- a/src/os/unix/ngx_posix_config.h +++ b/src/os/unix/ngx_posix_config.h @@ -124,7 +124,17 @@ #if (NGX_HAVE_KQUEUE) + #include + +/* NetBSD before 10.0 incompatibly defines kevent.udata as "intptr_t" */ + +#if (defined __NetBSD__ && __NetBSD_Version__ < 999001500) +#define NGX_KQUEUE_UDATA_T +#else +#define NGX_KQUEUE_UDATA_T (void *) +#endif + #endif From mdounin at mdounin.ru Tue May 7 05:43:30 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 07 May 2024 08:43:30 +0300 Subject: [PATCH] Tests: fixed http_listen.t on platforms without IPv6 Message-ID: <2e3736daf409fdb12ac2.1715060610@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1715050906 -10800 # Tue May 07 06:01:46 2024 +0300 # Node ID 2e3736daf409fdb12ac27ab123292dac4d8e4940 # Parent b5036a0f9ae0b3066fb75a5e33441e1c74f25bf4 Tests: fixed http_listen.t on platforms without IPv6. diff --git a/http_listen.t b/http_listen.t --- a/http_listen.t +++ b/http_listen.t @@ -71,7 +71,7 @@ my $p2 = port(8182); plan(skip_all => 'no requested ranges') if "$p2$p3" ne "81828183"; -$t->run()->plan(9); +$t->try_run('no inet6 support')->plan(9); ############################################################################### From maksim.yevmenkin at gmail.com Tue May 7 21:33:21 2024 From: maksim.yevmenkin at gmail.com (Maksim Yevmenkin) Date: Tue, 7 May 2024 14:33:21 -0700 Subject: [patch] reject http header without colon (:) in the header name Message-ID: hello, it appears that nginx would happily accept http header without colon (:) in the header name. the patch below tries to address this. thanks max == --- a/ports/netflix/nginx/files/nginx/src/http/ngx_http_parse.c +++ b/ports/netflix/nginx/files/nginx/src/http/ngx_http_parse.c @@ -941,14 +941,14 @@ ngx_http_parse_header_line(ngx_http_request_t *r, ngx_buf_t *b, r->header_start = p; r->header_end = p; state = sw_almost_done; - break; + return NGX_HTTP_PARSE_INVALID_HEADER; } if (ch == LF) { r->header_name_end = p; r->header_start = p; r->header_end = p; - goto done; + return NGX_HTTP_PARSE_INVALID_HEADER; } /* IIS may send the duplicate "HTTP/1.1 ..." lines */ From mdounin at mdounin.ru Tue May 7 23:16:46 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Wed, 8 May 2024 02:16:46 +0300 Subject: [patch] reject http header without colon (:) in the header name In-Reply-To: References: Message-ID: Hello! (It looks like you aren't subscribed to the mailing list, hence Cc.) On Tue, May 07, 2024 at 02:33:21PM -0700, Maksim Yevmenkin wrote: > hello, > > it appears that nginx would happily accept http header without colon > (:) in the header name. the patch below tries to address this. Yes. I believe it was allowed as a part of the following change in nginx 0.1.29 (509:9b8c906f6e63, https://freenginx.org/hg/nginx/rev/9b8c906f6e63#l85.68): *) Change: nginx now passes the invalid lines in a client request headers or a backend response header. With introduction of "ignore_invalid_headers" in nginx 0.1.30 (511:c12967aadd87) this wasn't changed though, which is probably an oversight. Such headers are essentially recognized as headers with an empty value, and I don't think that this can be an issue. Still, I don't object hardening the parser and rejecting such headers. I don't think this change will affect any real clients, though potentially might affect some misbehaving backend code, such as loosely written fastcgi scripts. If its the case, such code needs to be fixed. Alternatively, we can consider marking such headers as invalid, so they will be ignored from clients (unless "ignore_invalid_headers off;" is specified) and passed from backend servers to clients. Not sure it worth the effort though. > --- a/ports/netflix/nginx/files/nginx/src/http/ngx_http_parse.c > +++ b/ports/netflix/nginx/files/nginx/src/http/ngx_http_parse.c > @@ -941,14 +941,14 @@ ngx_http_parse_header_line(ngx_http_request_t > *r, ngx_buf_t *b, > r->header_start = p; > r->header_end = p; > state = sw_almost_done; > - break; > + return NGX_HTTP_PARSE_INVALID_HEADER; > } > > if (ch == LF) { > r->header_name_end = p; > r->header_start = p; > r->header_end = p; > - goto done; > + return NGX_HTTP_PARSE_INVALID_HEADER; > } > > /* IIS may send the duplicate "HTTP/1.1 ..." lines */ Removing relevant if blocks should be enough, these will be handled by the following "if (ch <= 0x20...)" block. Please take a look at the following patch: # HG changeset patch # User Maxim Dounin # Date 1715122480 -10800 # Wed May 08 01:54:40 2024 +0300 # Node ID e9aa91a1861dcabfd1e9ebb7d5d96cdb42c8dcd4 # Parent 93bbb9fbf30dd82709551610f05e22eac17717d4 Disabled handling of headers without a colon. Starting with nginx 0.1.29 (509:9b8c906f6e63), header names not followed by a colon and a value were allowed. Such headers were interpreted as headers with an empty value. With this change, such headers are unconditionally rejected. Requested by Maksim Yevmenkin. diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -961,21 +961,6 @@ ngx_http_parse_header_line(ngx_http_requ break; } - if (ch == CR) { - r->header_name_end = p; - r->header_start = p; - r->header_end = p; - state = sw_almost_done; - break; - } - - if (ch == LF) { - r->header_name_end = p; - r->header_start = p; - r->header_end = p; - goto done; - } - /* IIS may send the duplicate "HTTP/1.1 ..." lines */ if (ch == '/' && r->upstream -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Wed May 8 01:12:07 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 08 May 2024 04:12:07 +0300 Subject: [PATCH] Core: made it possible to disable PID files with "pid off" Message-ID: # HG changeset patch # User Maxim Dounin # Date 1715130688 -10800 # Wed May 08 04:11:28 2024 +0300 # Node ID b1bf4da2220d8c5321cbc8a7c873c61129b66a05 # Parent 93bbb9fbf30dd82709551610f05e22eac17717d4 Core: made it possible to disable PID files with "pid off". While it is not generally recommended, this might be beneficial in some configurations, such as with immutable images and direct control by a service manager. diff --git a/src/core/nginx.c b/src/core/nginx.c --- a/src/core/nginx.c +++ b/src/core/nginx.c @@ -766,7 +766,9 @@ ngx_exec_new_binary(ngx_cycle_t *cycle, ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module); - if (ngx_rename_file(ccf->pid.data, ccf->oldpid.data) == NGX_FILE_ERROR) { + if (ccf->pid.len + && ngx_rename_file(ccf->pid.data, ccf->oldpid.data) == NGX_FILE_ERROR) + { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, ngx_rename_file_n " %s to %s failed " "before executing new binary process \"%s\"", @@ -781,8 +783,9 @@ ngx_exec_new_binary(ngx_cycle_t *cycle, pid = ngx_execute(cycle, &ctx); if (pid == NGX_INVALID_PID) { - if (ngx_rename_file(ccf->oldpid.data, ccf->pid.data) - == NGX_FILE_ERROR) + if (ccf->pid.len + && ngx_rename_file(ccf->oldpid.data, ccf->pid.data) + == NGX_FILE_ERROR) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, ngx_rename_file_n " %s back to %s failed after " @@ -1168,20 +1171,26 @@ ngx_core_module_init_conf(ngx_cycle_t *c ngx_str_set(&ccf->pid, NGX_PID_PATH); } - if (ngx_conf_full_name(cycle, &ccf->pid, 0) != NGX_OK) { - return NGX_CONF_ERROR; - } + if (ngx_strcmp(ccf->pid.data, "off") == 0) { + ngx_str_set(&ccf->pid, ""); + ngx_str_set(&ccf->oldpid, ""); - ccf->oldpid.len = ccf->pid.len + sizeof(NGX_OLDPID_EXT); + } else { + if (ngx_conf_full_name(cycle, &ccf->pid, 0) != NGX_OK) { + return NGX_CONF_ERROR; + } - ccf->oldpid.data = ngx_pnalloc(cycle->pool, ccf->oldpid.len); - if (ccf->oldpid.data == NULL) { - return NGX_CONF_ERROR; + ccf->oldpid.len = ccf->pid.len + sizeof(NGX_OLDPID_EXT); + + ccf->oldpid.data = ngx_pnalloc(cycle->pool, ccf->oldpid.len); + if (ccf->oldpid.data == NULL) { + return NGX_CONF_ERROR; + } + + ngx_memcpy(ngx_cpymem(ccf->oldpid.data, ccf->pid.data, ccf->pid.len), + NGX_OLDPID_EXT, sizeof(NGX_OLDPID_EXT)); } - ngx_memcpy(ngx_cpymem(ccf->oldpid.data, ccf->pid.data, ccf->pid.len), - NGX_OLDPID_EXT, sizeof(NGX_OLDPID_EXT)); - #if !(NGX_WIN32) diff --git a/src/core/ngx_cycle.c b/src/core/ngx_cycle.c --- a/src/core/ngx_cycle.c +++ b/src/core/ngx_cycle.c @@ -1027,6 +1027,10 @@ ngx_create_pidfile(ngx_str_t *name, ngx_ return NGX_OK; } + if (name->len == 0) { + return NGX_OK; + } + ngx_memzero(&file, sizeof(ngx_file_t)); file.name = *name; @@ -1070,6 +1074,10 @@ ngx_delete_pidfile(ngx_cycle_t *cycle) ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module); + if (ccf->pid.len == 0) { + return; + } + name = ngx_new_binary ? ccf->oldpid.data : ccf->pid.data; if (ngx_delete_file(name) == NGX_FILE_ERROR) { @@ -1092,6 +1100,12 @@ ngx_signal_process(ngx_cycle_t *cycle, c ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module); + if (ccf->pid.len == 0) { + ngx_log_error(NGX_LOG_ERR, cycle->log, 0, + "no PID file configured"); + return 1; + } + ngx_memzero(&file, sizeof(ngx_file_t)); file.name = ccf->pid; diff --git a/src/os/unix/ngx_process_cycle.c b/src/os/unix/ngx_process_cycle.c --- a/src/os/unix/ngx_process_cycle.c +++ b/src/os/unix/ngx_process_cycle.c @@ -619,9 +619,10 @@ ngx_reap_children(ngx_cycle_t *cycle) ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module); - if (ngx_rename_file((char *) ccf->oldpid.data, - (char *) ccf->pid.data) - == NGX_FILE_ERROR) + if (ccf->pid.len + && ngx_rename_file((char *) ccf->oldpid.data, + (char *) ccf->pid.data) + == NGX_FILE_ERROR) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, ngx_rename_file_n " %s back to %s failed " From mdounin at mdounin.ru Wed May 8 01:13:55 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 08 May 2024 04:13:55 +0300 Subject: [PATCH] Core: added realpath() checking when testing PID files Message-ID: <72cfccd6d587eb0b0e8f.1715130835@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1715130825 -10800 # Wed May 08 04:13:45 2024 +0300 # Node ID 72cfccd6d587eb0b0e8f8c3c85e6bcecdc246cfe # Parent b1bf4da2220d8c5321cbc8a7c873c61129b66a05 Core: added realpath() checking when testing PID files. This ensures that if the PID file path is changed, yet resolves to the same file via symbolic links, trying to recreate the PID file won't remove it. In particular, this resolves issues as observed on Linux systems with "/var/run/nginx.pid" changed to "/run/nginx.pid". diff --git a/src/core/ngx_cycle.c b/src/core/ngx_cycle.c --- a/src/core/ngx_cycle.c +++ b/src/core/ngx_cycle.c @@ -13,6 +13,8 @@ static void ngx_destroy_cycle_pools(ngx_conf_t *conf); static ngx_int_t ngx_init_zone_pool(ngx_cycle_t *cycle, ngx_shm_zone_t *shm_zone); +static ngx_int_t ngx_pidfile_changed(ngx_str_t *name1, ngx_str_t *name2, + ngx_log_t *log); static ngx_int_t ngx_test_lockfile(u_char *file, ngx_log_t *log); static void ngx_clean_old_cycles(ngx_event_t *ev); static void ngx_shutdown_timer_handler(ngx_event_t *ev); @@ -332,9 +334,9 @@ ngx_init_cycle(ngx_cycle_t *old_cycle) old_ccf = (ngx_core_conf_t *) ngx_get_conf(old_cycle->conf_ctx, ngx_core_module); - if (ccf->pid.len != old_ccf->pid.len - || ngx_strcmp(ccf->pid.data, old_ccf->pid.data) != 0) - { + + if (ngx_pidfile_changed(&ccf->pid, &old_ccf->pid, log)) { + /* new pid file name */ if (ngx_create_pidfile(&ccf->pid, log) != NGX_OK) { @@ -1087,6 +1089,54 @@ ngx_delete_pidfile(ngx_cycle_t *cycle) } +static ngx_int_t +ngx_pidfile_changed(ngx_str_t *name1, ngx_str_t *name2, ngx_log_t *log) +{ + u_char *real1, *real2; + ngx_int_t rc; + + if (name1->len == name2->len + && ngx_strcmp(name1->data, name2->data) == 0) + { + return 0; + } + + rc = 1; + real1 = NULL; + real2 = NULL; + + real1 = ngx_realpath(name1->data, NULL); + + if (real1 == NULL) { + ngx_log_debug1(NGX_LOG_DEBUG_CORE, log, ngx_errno, + ngx_realpath_n " \"%s\" failed", name1->data); + goto done; + } + + real2 = ngx_realpath(name2->data, NULL); + + if (real2 == NULL) { + ngx_log_debug1(NGX_LOG_DEBUG_CORE, log, ngx_errno, + ngx_realpath_n " \"%s\" failed", name2->data); + goto done; + } + + rc = ngx_strcmp(real1, real2); + +done: + + if (real1 && real1 != name1->data) { + ngx_free(real1); + } + + if (real2 && real2 != name2->data) { + ngx_free(real2); + } + + return rc; +} + + ngx_int_t ngx_signal_process(ngx_cycle_t *cycle, char *sig) { From mdounin at mdounin.ru Wed May 8 01:14:30 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 08 May 2024 04:14:30 +0300 Subject: [PATCH] Core: PID file writing synchronization Message-ID: <06cfb3c166612afb9c72.1715130870@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1715130855 -10800 # Wed May 08 04:14:15 2024 +0300 # Node ID 06cfb3c166612afb9c7275762ab8e48d50024c20 # Parent 72cfccd6d587eb0b0e8f8c3c85e6bcecdc246cfe Core: PID file writing synchronization. Now, ngx_daemon() does not call exit() in the parent process immediately, but instead waits for the child process to signal it actually started (and wrote the PID file if configured to). This ensures that the PID file already exists when the parent process exits. To make sure that signal handlers won't cause unexpected logging in the parent process if the child process dies (for example, due to errors when writing the PID file), ngx_init_signals() is moved to the child process. This resolves "PID file ... not readable (yet?) after start" and "Failed to parse PID from file..." errors as observed with systemd. Note that the errors observed are considered to be a bug in systemd, which isn't able to work properly with traditional Unix daemons. Still, the workaround is implemented to make sure there will be no OS vendor patches trying to address this. diff --git a/src/core/nginx.c b/src/core/nginx.c --- a/src/core/nginx.c +++ b/src/core/nginx.c @@ -342,10 +342,6 @@ main(int argc, char *const *argv) #if !(NGX_WIN32) - if (ngx_init_signals(cycle->log) != NGX_OK) { - return 1; - } - if (!ngx_inherited && ccf->daemon) { if (ngx_daemon(cycle->log) != NGX_OK) { return 1; @@ -364,6 +360,18 @@ main(int argc, char *const *argv) return 1; } +#if !(NGX_WIN32) + + if (ngx_init_signals(cycle->log) != NGX_OK) { + return 1; + } + + if (ngx_daemon_sync(cycle->log) != NGX_OK) { + return 1; + } + +#endif + if (ngx_log_redirect_stderr(cycle) != NGX_OK) { return 1; } diff --git a/src/os/unix/ngx_daemon.c b/src/os/unix/ngx_daemon.c --- a/src/os/unix/ngx_daemon.c +++ b/src/os/unix/ngx_daemon.c @@ -9,10 +9,20 @@ #include +static ngx_fd_t ngx_daemon_fd = NGX_INVALID_FILE; + + ngx_int_t ngx_daemon(ngx_log_t *log) { - int fd; + u_char buf[1]; + ssize_t n; + ngx_fd_t fd, pp[2]; + + if (pipe(pp) == -1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "pipe() failed"); + return NGX_ERROR; + } switch (fork()) { case -1: @@ -20,9 +30,38 @@ ngx_daemon(ngx_log_t *log) return NGX_ERROR; case 0: + if (close(pp[0]) == -1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() pipe failed"); + return NGX_ERROR; + } + + ngx_daemon_fd = pp[1]; break; default: + if (close(pp[1]) == -1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() pipe failed"); + return NGX_ERROR; + } + + n = read(pp[0], buf, 1); + + if (n == 0) { + /* child exited */ + return NGX_ERROR; + } + + if (n != 1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, + "read() pipe failed"); + return NGX_ERROR; + } + + if (close(pp[0]) == -1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() pipe failed"); + return NGX_ERROR; + } + exit(0); } @@ -69,3 +108,26 @@ ngx_daemon(ngx_log_t *log) return NGX_OK; } + + +ngx_int_t +ngx_daemon_sync(ngx_log_t *log) +{ + if (ngx_daemon_fd == NGX_INVALID_FILE) { + return NGX_OK; + } + + if (write(ngx_daemon_fd, "", 1) != 1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "write() pipe failed"); + return NGX_ERROR; + } + + if (close(ngx_daemon_fd) == -1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() pipe failed"); + return NGX_ERROR; + } + + ngx_daemon_fd = NGX_INVALID_FILE; + + return NGX_OK; +} diff --git a/src/os/unix/ngx_os.h b/src/os/unix/ngx_os.h --- a/src/os/unix/ngx_os.h +++ b/src/os/unix/ngx_os.h @@ -40,6 +40,7 @@ void ngx_os_status(ngx_log_t *log); ngx_int_t ngx_os_specific_init(ngx_log_t *log); void ngx_os_specific_status(ngx_log_t *log); ngx_int_t ngx_daemon(ngx_log_t *log); +ngx_int_t ngx_daemon_sync(ngx_log_t *log); ngx_int_t ngx_os_signal_process(ngx_cycle_t *cycle, char *sig, ngx_pid_t pid); From maksim.yevmenkin at gmail.com Wed May 8 17:59:30 2024 From: maksim.yevmenkin at gmail.com (Maksim Yevmenkin) Date: Wed, 8 May 2024 10:59:30 -0700 Subject: [patch] reject http header without colon (:) in the header name In-Reply-To: References: Message-ID: > > it appears that nginx would happily accept http header without colon > > (:) in the header name. the patch below tries to address this. > > Yes. I believe it was allowed as a part of the following change > in nginx 0.1.29 (509:9b8c906f6e63, > https://freenginx.org/hg/nginx/rev/9b8c906f6e63#l85.68): > > *) Change: nginx now passes the invalid lines in a client request > headers or a backend response header. > > With introduction of "ignore_invalid_headers" in nginx 0.1.30 > (511:c12967aadd87) this wasn't changed though, which is probably > an oversight. > > Such headers are essentially recognized as headers with an > empty value, and I don't think that this can be an issue. Still, > I don't object hardening the parser and rejecting such headers. > > I don't think this change will affect any real clients, though > potentially might affect some misbehaving backend code, such as > loosely written fastcgi scripts. If its the case, such code needs > to be fixed. > > Alternatively, we can consider marking such headers as invalid, so > they will be ignored from clients (unless "ignore_invalid_headers off;" > is specified) and passed from backend servers to clients. Not > sure it worth the effort though. [...] > Removing relevant if blocks should be enough, these will be > handled by the following "if (ch <= 0x20...)" block. Please take > a look at the following patch: > > # HG changeset patch > # User Maxim Dounin > # Date 1715122480 -10800 > # Wed May 08 01:54:40 2024 +0300 > # Node ID e9aa91a1861dcabfd1e9ebb7d5d96cdb42c8dcd4 > # Parent 93bbb9fbf30dd82709551610f05e22eac17717d4 > Disabled handling of headers without a colon. > > Starting with nginx 0.1.29 (509:9b8c906f6e63), header names not followed > by a colon and a value were allowed. Such headers were interpreted as > headers with an empty value. With this change, such headers are > unconditionally rejected. > > Requested by Maksim Yevmenkin. > > diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c > --- a/src/http/ngx_http_parse.c > +++ b/src/http/ngx_http_parse.c > @@ -961,21 +961,6 @@ ngx_http_parse_header_line(ngx_http_requ > break; > } > > - if (ch == CR) { > - r->header_name_end = p; > - r->header_start = p; > - r->header_end = p; > - state = sw_almost_done; > - break; > - } > - > - if (ch == LF) { > - r->header_name_end = p; > - r->header_start = p; > - r->header_end = p; > - goto done; > - } > - > /* IIS may send the duplicate "HTTP/1.1 ..." lines */ > if (ch == '/' > && r->upstream this patch works. !thanks max From mdounin at mdounin.ru Wed May 8 20:50:14 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Wed, 08 May 2024 23:50:14 +0300 Subject: [nginx] Disabled handling of headers without a colon. Message-ID: details: http://freenginx.org/hg/nginx/rev/9a5e2296c1be branches: changeset: 9267:9a5e2296c1be user: Maxim Dounin date: Wed May 08 23:00:07 2024 +0300 description: Disabled handling of headers without a colon. Starting with nginx 0.1.29 (509:9b8c906f6e63), header names not followed by a colon and a value were allowed. Such headers were interpreted as headers with an empty value. With this change, such headers are unconditionally rejected. Requested by Maksim Yevmenkin. diffstat: src/http/ngx_http_parse.c | 15 --------------- 1 files changed, 0 insertions(+), 15 deletions(-) diffs (25 lines): diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -961,21 +961,6 @@ ngx_http_parse_header_line(ngx_http_requ break; } - if (ch == CR) { - r->header_name_end = p; - r->header_start = p; - r->header_end = p; - state = sw_almost_done; - break; - } - - if (ch == LF) { - r->header_name_end = p; - r->header_start = p; - r->header_end = p; - goto done; - } - /* IIS may send the duplicate "HTTP/1.1 ..." lines */ if (ch == '/' && r->upstream From mdounin at mdounin.ru Wed May 8 20:52:20 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Wed, 8 May 2024 23:52:20 +0300 Subject: [patch] reject http header without colon (:) in the header name In-Reply-To: References: Message-ID: Hello! On Wed, May 08, 2024 at 10:59:30AM -0700, Maksim Yevmenkin wrote: > > > it appears that nginx would happily accept http header without colon > > > (:) in the header name. the patch below tries to address this. > > > > Yes. I believe it was allowed as a part of the following change > > in nginx 0.1.29 (509:9b8c906f6e63, > > https://freenginx.org/hg/nginx/rev/9b8c906f6e63#l85.68): > > > > *) Change: nginx now passes the invalid lines in a client request > > headers or a backend response header. > > > > With introduction of "ignore_invalid_headers" in nginx 0.1.30 > > (511:c12967aadd87) this wasn't changed though, which is probably > > an oversight. > > > > Such headers are essentially recognized as headers with an > > empty value, and I don't think that this can be an issue. Still, > > I don't object hardening the parser and rejecting such headers. > > > > I don't think this change will affect any real clients, though > > potentially might affect some misbehaving backend code, such as > > loosely written fastcgi scripts. If its the case, such code needs > > to be fixed. > > > > Alternatively, we can consider marking such headers as invalid, so > > they will be ignored from clients (unless "ignore_invalid_headers off;" > > is specified) and passed from backend servers to clients. Not > > sure it worth the effort though. > > [...] > > > Removing relevant if blocks should be enough, these will be > > handled by the following "if (ch <= 0x20...)" block. Please take > > a look at the following patch: > > > > # HG changeset patch > > # User Maxim Dounin > > # Date 1715122480 -10800 > > # Wed May 08 01:54:40 2024 +0300 > > # Node ID e9aa91a1861dcabfd1e9ebb7d5d96cdb42c8dcd4 > > # Parent 93bbb9fbf30dd82709551610f05e22eac17717d4 > > Disabled handling of headers without a colon. > > > > Starting with nginx 0.1.29 (509:9b8c906f6e63), header names not followed > > by a colon and a value were allowed. Such headers were interpreted as > > headers with an empty value. With this change, such headers are > > unconditionally rejected. > > > > Requested by Maksim Yevmenkin. > > > > diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c > > --- a/src/http/ngx_http_parse.c > > +++ b/src/http/ngx_http_parse.c > > @@ -961,21 +961,6 @@ ngx_http_parse_header_line(ngx_http_requ > > break; > > } > > > > - if (ch == CR) { > > - r->header_name_end = p; > > - r->header_start = p; > > - r->header_end = p; > > - state = sw_almost_done; > > - break; > > - } > > - > > - if (ch == LF) { > > - r->header_name_end = p; > > - r->header_start = p; > > - r->header_end = p; > > - goto done; > > - } > > - > > /* IIS may send the duplicate "HTTP/1.1 ..." lines */ > > if (ch == '/' > > && r->upstream > > this patch works. > > !thanks > max Committed, thanks for prodding this. -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Wed May 8 20:57:20 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Wed, 08 May 2024 23:57:20 +0300 Subject: [PATCH] Tests: added test for headers without a colon Message-ID: # HG changeset patch # User Maxim Dounin # Date 1715201571 -10800 # Wed May 08 23:52:51 2024 +0300 # Node ID e62a5943d54efd1f0d2380f9ecc0ea76de9d3163 # Parent 2e3736daf409fdb12ac27ab123292dac4d8e4940 Tests: added test for headers without a colon. diff --git a/http_request.t b/http_request.t --- a/http_request.t +++ b/http_request.t @@ -23,7 +23,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http rewrite/)->plan(40) +my $t = Test::Nginx->new()->has(qw/http rewrite/)->plan(41) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -187,4 +187,12 @@ like(http("GET / HTTP/1.0" . CRLF . "Foo like(http("GET / HTTP/1.0" . CRLF . "Foo\t: bar" . CRLF . CRLF), qr/ 400 /, 'header with tab rejected'); +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.27.0'); + +like(http("GET / HTTP/1.0" . CRLF . "Foo" . CRLF . CRLF), qr/ 400 /, + 'header without colon rejected'); + +} + ############################################################################### From mdounin at mdounin.ru Sat May 11 15:56:16 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 11 May 2024 18:56:16 +0300 Subject: [nginx-tests] Tests: fixed http_listen.t on platforms without IPv6. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/e6b20af0be5b branches: changeset: 1975:e6b20af0be5b user: Maxim Dounin date: Sat May 11 18:55:25 2024 +0300 description: Tests: fixed http_listen.t on platforms without IPv6. diffstat: http_listen.t | 2 +- 1 files changed, 1 insertions(+), 1 deletions(-) diffs (12 lines): diff --git a/http_listen.t b/http_listen.t --- a/http_listen.t +++ b/http_listen.t @@ -71,7 +71,7 @@ my $p2 = port(8182); plan(skip_all => 'no requested ranges') if "$p2$p3" ne "81828183"; -$t->run()->plan(9); +$t->try_run('no inet6 support')->plan(9); ############################################################################### From mdounin at mdounin.ru Sat May 11 15:57:02 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sat, 11 May 2024 18:57:02 +0300 Subject: [nginx-tests] Tests: added test for headers without a colon. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/4e79bd25642f branches: changeset: 1976:4e79bd25642f user: Maxim Dounin date: Sat May 11 18:56:23 2024 +0300 description: Tests: added test for headers without a colon. diffstat: http_request.t | 10 +++++++++- 1 files changed, 9 insertions(+), 1 deletions(-) diffs (25 lines): diff --git a/http_request.t b/http_request.t --- a/http_request.t +++ b/http_request.t @@ -23,7 +23,7 @@ use Test::Nginx; select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http rewrite/)->plan(40) +my $t = Test::Nginx->new()->has(qw/http rewrite/)->plan(41) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -187,4 +187,12 @@ like(http("GET / HTTP/1.0" . CRLF . "Foo like(http("GET / HTTP/1.0" . CRLF . "Foo\t: bar" . CRLF . CRLF), qr/ 400 /, 'header with tab rejected'); +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.27.0'); + +like(http("GET / HTTP/1.0" . CRLF . "Foo" . CRLF . CRLF), qr/ 400 /, + 'header without colon rejected'); + +} + ############################################################################### From mdounin at mdounin.ru Mon May 13 03:36:07 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 13 May 2024 06:36:07 +0300 Subject: [nginx] Core: made it possible to disable PID files with "pid off". Message-ID: details: http://freenginx.org/hg/nginx/rev/6a3ee145d0b5 branches: changeset: 9268:6a3ee145d0b5 user: Maxim Dounin date: Mon May 13 06:13:03 2024 +0300 description: Core: made it possible to disable PID files with "pid off". While it is not generally recommended, this might be beneficial in some configurations, such as with immutable images and direct control by a service manager. diffstat: src/core/nginx.c | 35 ++++++++++++++++++++++------------- src/core/ngx_cycle.c | 14 ++++++++++++++ src/os/unix/ngx_process_cycle.c | 7 ++++--- 3 files changed, 40 insertions(+), 16 deletions(-) diffs (118 lines): diff --git a/src/core/nginx.c b/src/core/nginx.c --- a/src/core/nginx.c +++ b/src/core/nginx.c @@ -766,7 +766,9 @@ ngx_exec_new_binary(ngx_cycle_t *cycle, ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module); - if (ngx_rename_file(ccf->pid.data, ccf->oldpid.data) == NGX_FILE_ERROR) { + if (ccf->pid.len + && ngx_rename_file(ccf->pid.data, ccf->oldpid.data) == NGX_FILE_ERROR) + { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, ngx_rename_file_n " %s to %s failed " "before executing new binary process \"%s\"", @@ -781,8 +783,9 @@ ngx_exec_new_binary(ngx_cycle_t *cycle, pid = ngx_execute(cycle, &ctx); if (pid == NGX_INVALID_PID) { - if (ngx_rename_file(ccf->oldpid.data, ccf->pid.data) - == NGX_FILE_ERROR) + if (ccf->pid.len + && ngx_rename_file(ccf->oldpid.data, ccf->pid.data) + == NGX_FILE_ERROR) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, ngx_rename_file_n " %s back to %s failed after " @@ -1168,20 +1171,26 @@ ngx_core_module_init_conf(ngx_cycle_t *c ngx_str_set(&ccf->pid, NGX_PID_PATH); } - if (ngx_conf_full_name(cycle, &ccf->pid, 0) != NGX_OK) { - return NGX_CONF_ERROR; - } + if (ngx_strcmp(ccf->pid.data, "off") == 0) { + ngx_str_set(&ccf->pid, ""); + ngx_str_set(&ccf->oldpid, ""); - ccf->oldpid.len = ccf->pid.len + sizeof(NGX_OLDPID_EXT); + } else { + if (ngx_conf_full_name(cycle, &ccf->pid, 0) != NGX_OK) { + return NGX_CONF_ERROR; + } - ccf->oldpid.data = ngx_pnalloc(cycle->pool, ccf->oldpid.len); - if (ccf->oldpid.data == NULL) { - return NGX_CONF_ERROR; + ccf->oldpid.len = ccf->pid.len + sizeof(NGX_OLDPID_EXT); + + ccf->oldpid.data = ngx_pnalloc(cycle->pool, ccf->oldpid.len); + if (ccf->oldpid.data == NULL) { + return NGX_CONF_ERROR; + } + + ngx_memcpy(ngx_cpymem(ccf->oldpid.data, ccf->pid.data, ccf->pid.len), + NGX_OLDPID_EXT, sizeof(NGX_OLDPID_EXT)); } - ngx_memcpy(ngx_cpymem(ccf->oldpid.data, ccf->pid.data, ccf->pid.len), - NGX_OLDPID_EXT, sizeof(NGX_OLDPID_EXT)); - #if !(NGX_WIN32) diff --git a/src/core/ngx_cycle.c b/src/core/ngx_cycle.c --- a/src/core/ngx_cycle.c +++ b/src/core/ngx_cycle.c @@ -1027,6 +1027,10 @@ ngx_create_pidfile(ngx_str_t *name, ngx_ return NGX_OK; } + if (name->len == 0) { + return NGX_OK; + } + ngx_memzero(&file, sizeof(ngx_file_t)); file.name = *name; @@ -1070,6 +1074,10 @@ ngx_delete_pidfile(ngx_cycle_t *cycle) ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module); + if (ccf->pid.len == 0) { + return; + } + name = ngx_new_binary ? ccf->oldpid.data : ccf->pid.data; if (ngx_delete_file(name) == NGX_FILE_ERROR) { @@ -1092,6 +1100,12 @@ ngx_signal_process(ngx_cycle_t *cycle, c ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module); + if (ccf->pid.len == 0) { + ngx_log_error(NGX_LOG_ERR, cycle->log, 0, + "no PID file configured"); + return 1; + } + ngx_memzero(&file, sizeof(ngx_file_t)); file.name = ccf->pid; diff --git a/src/os/unix/ngx_process_cycle.c b/src/os/unix/ngx_process_cycle.c --- a/src/os/unix/ngx_process_cycle.c +++ b/src/os/unix/ngx_process_cycle.c @@ -619,9 +619,10 @@ ngx_reap_children(ngx_cycle_t *cycle) ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module); - if (ngx_rename_file((char *) ccf->oldpid.data, - (char *) ccf->pid.data) - == NGX_FILE_ERROR) + if (ccf->pid.len + && ngx_rename_file((char *) ccf->oldpid.data, + (char *) ccf->pid.data) + == NGX_FILE_ERROR) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, ngx_rename_file_n " %s back to %s failed " From mdounin at mdounin.ru Mon May 13 03:36:07 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 13 May 2024 06:36:07 +0300 Subject: [nginx] Core: added realpath() checking when testing PID files. Message-ID: details: http://freenginx.org/hg/nginx/rev/4eb02e5ddb48 branches: changeset: 9269:4eb02e5ddb48 user: Maxim Dounin date: Mon May 13 06:13:12 2024 +0300 description: Core: added realpath() checking when testing PID files. This ensures that if the PID file path is changed, yet resolves to the same file via symbolic links, trying to recreate the PID file won't remove it. In particular, this resolves issues as observed on Linux systems with "/var/run/nginx.pid" changed to "/run/nginx.pid". diffstat: src/core/ngx_cycle.c | 56 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 files changed, 53 insertions(+), 3 deletions(-) diffs (80 lines): diff --git a/src/core/ngx_cycle.c b/src/core/ngx_cycle.c --- a/src/core/ngx_cycle.c +++ b/src/core/ngx_cycle.c @@ -13,6 +13,8 @@ static void ngx_destroy_cycle_pools(ngx_conf_t *conf); static ngx_int_t ngx_init_zone_pool(ngx_cycle_t *cycle, ngx_shm_zone_t *shm_zone); +static ngx_int_t ngx_pidfile_changed(ngx_str_t *name1, ngx_str_t *name2, + ngx_log_t *log); static ngx_int_t ngx_test_lockfile(u_char *file, ngx_log_t *log); static void ngx_clean_old_cycles(ngx_event_t *ev); static void ngx_shutdown_timer_handler(ngx_event_t *ev); @@ -332,9 +334,9 @@ ngx_init_cycle(ngx_cycle_t *old_cycle) old_ccf = (ngx_core_conf_t *) ngx_get_conf(old_cycle->conf_ctx, ngx_core_module); - if (ccf->pid.len != old_ccf->pid.len - || ngx_strcmp(ccf->pid.data, old_ccf->pid.data) != 0) - { + + if (ngx_pidfile_changed(&ccf->pid, &old_ccf->pid, log)) { + /* new pid file name */ if (ngx_create_pidfile(&ccf->pid, log) != NGX_OK) { @@ -1087,6 +1089,54 @@ ngx_delete_pidfile(ngx_cycle_t *cycle) } +static ngx_int_t +ngx_pidfile_changed(ngx_str_t *name1, ngx_str_t *name2, ngx_log_t *log) +{ + u_char *real1, *real2; + ngx_int_t rc; + + if (name1->len == name2->len + && ngx_strcmp(name1->data, name2->data) == 0) + { + return 0; + } + + rc = 1; + real1 = NULL; + real2 = NULL; + + real1 = ngx_realpath(name1->data, NULL); + + if (real1 == NULL) { + ngx_log_debug1(NGX_LOG_DEBUG_CORE, log, ngx_errno, + ngx_realpath_n " \"%s\" failed", name1->data); + goto done; + } + + real2 = ngx_realpath(name2->data, NULL); + + if (real2 == NULL) { + ngx_log_debug1(NGX_LOG_DEBUG_CORE, log, ngx_errno, + ngx_realpath_n " \"%s\" failed", name2->data); + goto done; + } + + rc = ngx_strcmp(real1, real2); + +done: + + if (real1 && real1 != name1->data) { + ngx_free(real1); + } + + if (real2 && real2 != name2->data) { + ngx_free(real2); + } + + return rc; +} + + ngx_int_t ngx_signal_process(ngx_cycle_t *cycle, char *sig) { From mdounin at mdounin.ru Mon May 13 03:36:07 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 13 May 2024 06:36:07 +0300 Subject: [nginx] Core: PID file writing synchronization. Message-ID: details: http://freenginx.org/hg/nginx/rev/3d455e37abf8 branches: changeset: 9270:3d455e37abf8 user: Maxim Dounin date: Mon May 13 06:13:22 2024 +0300 description: Core: PID file writing synchronization. Now, ngx_daemon() does not call exit() in the parent process immediately, but instead waits for the child process to signal it actually started (and wrote the PID file if configured to). This ensures that the PID file already exists when the parent process exits. To make sure that signal handlers won't cause unexpected logging in the parent process if the child process dies (for example, due to errors when writing the PID file), ngx_init_signals() is moved to the child process. This resolves "PID file ... not readable (yet?) after start" and "Failed to parse PID from file..." errors as observed with systemd. Note that the errors observed are considered to be a bug in systemd, which isn't able to work properly with traditional Unix daemons. Still, the workaround is implemented to make sure there will be no OS vendor patches trying to address this. diffstat: src/core/nginx.c | 16 +++++++++--- src/os/unix/ngx_daemon.c | 64 +++++++++++++++++++++++++++++++++++++++++++++++- src/os/unix/ngx_os.h | 1 + 3 files changed, 76 insertions(+), 5 deletions(-) diffs (135 lines): diff --git a/src/core/nginx.c b/src/core/nginx.c --- a/src/core/nginx.c +++ b/src/core/nginx.c @@ -342,10 +342,6 @@ main(int argc, char *const *argv) #if !(NGX_WIN32) - if (ngx_init_signals(cycle->log) != NGX_OK) { - return 1; - } - if (!ngx_inherited && ccf->daemon) { if (ngx_daemon(cycle->log) != NGX_OK) { return 1; @@ -364,6 +360,18 @@ main(int argc, char *const *argv) return 1; } +#if !(NGX_WIN32) + + if (ngx_init_signals(cycle->log) != NGX_OK) { + return 1; + } + + if (ngx_daemon_sync(cycle->log) != NGX_OK) { + return 1; + } + +#endif + if (ngx_log_redirect_stderr(cycle) != NGX_OK) { return 1; } diff --git a/src/os/unix/ngx_daemon.c b/src/os/unix/ngx_daemon.c --- a/src/os/unix/ngx_daemon.c +++ b/src/os/unix/ngx_daemon.c @@ -9,10 +9,20 @@ #include +static ngx_fd_t ngx_daemon_fd = NGX_INVALID_FILE; + + ngx_int_t ngx_daemon(ngx_log_t *log) { - int fd; + u_char buf[1]; + ssize_t n; + ngx_fd_t fd, pp[2]; + + if (pipe(pp) == -1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "pipe() failed"); + return NGX_ERROR; + } switch (fork()) { case -1: @@ -20,9 +30,38 @@ ngx_daemon(ngx_log_t *log) return NGX_ERROR; case 0: + if (close(pp[0]) == -1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() pipe failed"); + return NGX_ERROR; + } + + ngx_daemon_fd = pp[1]; break; default: + if (close(pp[1]) == -1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() pipe failed"); + return NGX_ERROR; + } + + n = read(pp[0], buf, 1); + + if (n == 0) { + /* child exited */ + return NGX_ERROR; + } + + if (n != 1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, + "read() pipe failed"); + return NGX_ERROR; + } + + if (close(pp[0]) == -1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() pipe failed"); + return NGX_ERROR; + } + exit(0); } @@ -69,3 +108,26 @@ ngx_daemon(ngx_log_t *log) return NGX_OK; } + + +ngx_int_t +ngx_daemon_sync(ngx_log_t *log) +{ + if (ngx_daemon_fd == NGX_INVALID_FILE) { + return NGX_OK; + } + + if (write(ngx_daemon_fd, "", 1) != 1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "write() pipe failed"); + return NGX_ERROR; + } + + if (close(ngx_daemon_fd) == -1) { + ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() pipe failed"); + return NGX_ERROR; + } + + ngx_daemon_fd = NGX_INVALID_FILE; + + return NGX_OK; +} diff --git a/src/os/unix/ngx_os.h b/src/os/unix/ngx_os.h --- a/src/os/unix/ngx_os.h +++ b/src/os/unix/ngx_os.h @@ -40,6 +40,7 @@ void ngx_os_status(ngx_log_t *log); ngx_int_t ngx_os_specific_init(ngx_log_t *log); void ngx_os_specific_status(ngx_log_t *log); ngx_int_t ngx_daemon(ngx_log_t *log); +ngx_int_t ngx_daemon_sync(ngx_log_t *log); ngx_int_t ngx_os_signal_process(ngx_cycle_t *cycle, char *sig, ngx_pid_t pid); From robm at fastmailteam.com Mon May 13 05:40:22 2024 From: robm at fastmailteam.com (Robert Mueller) Date: Mon, 13 May 2024 15:40:22 +1000 Subject: OAUTHBEARER and XOAUTH2 support for nginx mail proxy module Message-ID: <51cd7a4b-210e-470e-a209-6b5ff66ad558@app.fastmail.com> Hi I have a patch that adds OAUTHBEARER and XOAUTH2 support to the nginx mail proxy module. We've now been using a slightly updated version of the patch for well over a year at fastmail.com and it's been stable. As well as the implementation patch, I also have: 1. A set of patches against the test suite to test the new code fairly well 2. A patch to the test suite to optionally allow running nginx under valgrind to test for any unexpected memory accesses, leaks, etc 3. A small patch that makes nginx mail proxy run clean under valgrind 4. A small fix to the IMAP mail proxy module to correctly clear the tag if there's an incomplete next line after a failure One thing I don't have is a patch to the documentation. The auth server may return an additional optional response header `Auth-Error-Sasl`. It's expected in the auth failure case that the backend auth server will generate a base64 encoded JSON object that conforms to the error reporting in https://datatracker.ietf.org/doc/html/rfc7628#section-3.2.2 in this header. # TEST_NGINX_VALGRIND=1 prove mail_* mail_auth_sasl.t ............ ok mail_auth_wait.t ............ ok mail_capability.t ........... ok mail_error_log.t ............ ok mail_imap_ssl.t ............. ok mail_imap.t ................. ok mail_max_errors.t ........... ok mail_pop3.t ................. ok mail_proxy_protocol.t ....... ok mail_proxy_smtp_auth.t ...... ok mail_proxy_timeout.t ........ ok mail_resolver.t ............. ok mail_smtp_greeting_delay.t .. ok mail_smtp.t ................. ok mail_smtp_xclient.t ......... ok mail_ssl_conf_command.t ..... ok mail_ssl_session_reuse.t .... ok mail_ssl.t .................. ok All tests successful. Files=18, Tests=318, 71 wallclock secs ( 0.08 usr 0.02 sys + 26.03 cusr 1.36 csys = 27.49 CPU) Result: PASS Cheers Rob Mueller robm at fastmailteam.com From robm at fastmailteam.com Mon May 13 05:41:45 2024 From: robm at fastmailteam.com (Robert Mueller) Date: Mon, 13 May 2024 15:41:45 +1000 Subject: [nginx] Add support for XOAUTH2 and OAUTHBEARER authentication Message-ID: # HG changeset patch # User Rob Mueller Add support for XOAUTH2 and OAUTHBEARER authentication This patch adds support for RFC 7628 OAUTHBEARER style authentication to the nginx mail proxy module for IMAP, POP and SMTP. To help with legacy libraries, it also implements the pre-RFC XOAUTH2 protocol as well. It adds ngx_*_auth_{oauthbearer,xoauth2} states, constants, handlers, etc. The bearer token provided by the client is passed to the backend auth server in the `Auth-Pass` header. The auth server may return an additional optional response header `Auth-Error-Sasl`. It's expected in the auth failure case that the backend auth server will generate a base64 encoded JSON object that conforms to the error reporting in https://datatracker.ietf.org/doc/html/rfc7628#section-3.2.2 in this header. If present, the value in this header is prefixed with a `+ ` (IMAP) or `334 ` (SMTP or POP) and returned as the SASL response. We then wait for a valid line from the client (which we ignore) and then we exit the SASL mode and return back to standard protocol parsing. If we don't receive a valid line, we terminate the connection. There's an example of this looks like in https://datatracker.ietf.org/doc/html/rfc7628#section-4.3 --- src/mail/ngx_mail.h | 30 ++-- src/mail/ngx_mail_auth_http_module.c | 201 ++++++++++++++++++++++++++- src/mail/ngx_mail_handler.c | 33 +++++ src/mail/ngx_mail_imap_handler.c | 30 ++++ src/mail/ngx_mail_imap_module.c | 8 +- src/mail/ngx_mail_parse.c | 32 +++++ src/mail/ngx_mail_pop3_handler.c | 30 ++++ src/mail/ngx_mail_pop3_module.c | 12 +- src/mail/ngx_mail_smtp_handler.c | 30 ++++ src/mail/ngx_mail_smtp_module.c | 8 +- 10 files changed, 390 insertions(+), 24 deletions(-) diff --git a/src/mail/ngx_mail.h b/src/mail/ngx_mail.h index e0c62b7ab..ed95a5417 100644 --- a/src/mail/ngx_mail.h +++ b/src/mail/ngx_mail.h @@ -140,7 +140,9 @@ typedef enum { ngx_pop3_auth_login_password, ngx_pop3_auth_plain, ngx_pop3_auth_cram_md5, - ngx_pop3_auth_external + ngx_pop3_auth_external, + ngx_pop3_auth_xoauth2, + ngx_pop3_auth_oauthbearer, } ngx_pop3_state_e; @@ -151,6 +153,8 @@ typedef enum { ngx_imap_auth_plain, ngx_imap_auth_cram_md5, ngx_imap_auth_external, + ngx_imap_auth_xoauth2, + ngx_imap_auth_oauthbearer, ngx_imap_login, ngx_imap_user, ngx_imap_passwd @@ -164,6 +168,8 @@ typedef enum { ngx_smtp_auth_plain, ngx_smtp_auth_cram_md5, ngx_smtp_auth_external, + ngx_smtp_auth_xoauth2, + ngx_smtp_auth_oauthbearer, ngx_smtp_helo, ngx_smtp_helo_xclient, ngx_smtp_helo_auth, @@ -211,7 +217,7 @@ typedef struct { unsigned no_sync_literal:1; unsigned starttls:1; unsigned esmtp:1; - unsigned auth_method:3; + unsigned auth_method:4; unsigned auth_wait:1; ngx_str_t login; @@ -301,15 +307,19 @@ typedef struct { #define NGX_MAIL_AUTH_APOP 3 #define NGX_MAIL_AUTH_CRAM_MD5 4 #define NGX_MAIL_AUTH_EXTERNAL 5 -#define NGX_MAIL_AUTH_NONE 6 +#define NGX_MAIL_AUTH_XOAUTH2 6 +#define NGX_MAIL_AUTH_OAUTHBEARER 7 +#define NGX_MAIL_AUTH_NONE 8 -#define NGX_MAIL_AUTH_PLAIN_ENABLED 0x0002 -#define NGX_MAIL_AUTH_LOGIN_ENABLED 0x0004 -#define NGX_MAIL_AUTH_APOP_ENABLED 0x0008 -#define NGX_MAIL_AUTH_CRAM_MD5_ENABLED 0x0010 -#define NGX_MAIL_AUTH_EXTERNAL_ENABLED 0x0020 -#define NGX_MAIL_AUTH_NONE_ENABLED 0x0040 +#define NGX_MAIL_AUTH_PLAIN_ENABLED 0x0002 +#define NGX_MAIL_AUTH_LOGIN_ENABLED 0x0004 +#define NGX_MAIL_AUTH_APOP_ENABLED 0x0008 +#define NGX_MAIL_AUTH_CRAM_MD5_ENABLED 0x0010 +#define NGX_MAIL_AUTH_EXTERNAL_ENABLED 0x0020 +#define NGX_MAIL_AUTH_XOAUTH2_ENABLED 0x0040 +#define NGX_MAIL_AUTH_OAUTHBEARER_ENABLED 0x0080 +#define NGX_MAIL_AUTH_NONE_ENABLED 0x0100 #define NGX_MAIL_PARSE_INVALID_COMMAND 20 @@ -397,6 +407,8 @@ ngx_int_t ngx_mail_auth_cram_md5_salt(ngx_mail_session_t *s, ngx_int_t ngx_mail_auth_cram_md5(ngx_mail_session_t *s, ngx_connection_t *c); ngx_int_t ngx_mail_auth_external(ngx_mail_session_t *s, ngx_connection_t *c, ngx_uint_t n); +ngx_int_t ngx_mail_auth_oauth(ngx_mail_session_t *s, ngx_connection_t *c, + ngx_uint_t n, ngx_uint_t auth_method); ngx_int_t ngx_mail_auth_parse(ngx_mail_session_t *s, ngx_connection_t *c); void ngx_mail_send(ngx_event_t *wev); diff --git a/src/mail/ngx_mail_auth_http_module.c b/src/mail/ngx_mail_auth_http_module.c index 27f64b92e..543d1c4eb 100644 --- a/src/mail/ngx_mail_auth_http_module.c +++ b/src/mail/ngx_mail_auth_http_module.c @@ -53,6 +53,7 @@ struct ngx_mail_auth_http_ctx_s { ngx_str_t err; ngx_str_t errmsg; ngx_str_t errcode; + ngx_str_t errsasl; time_t sleep; @@ -66,6 +67,7 @@ static void ngx_mail_auth_http_ignore_status_line(ngx_mail_session_t *s, ngx_mail_auth_http_ctx_t *ctx); static void ngx_mail_auth_http_process_headers(ngx_mail_session_t *s, ngx_mail_auth_http_ctx_t *ctx); +static void ngx_mail_read_sasl_end_handler(ngx_event_t *rev); static void ngx_mail_auth_sleep_handler(ngx_event_t *rev); static ngx_int_t ngx_mail_auth_http_parse_header_line(ngx_mail_session_t *s, ngx_mail_auth_http_ctx_t *ctx); @@ -152,6 +154,8 @@ static ngx_str_t ngx_mail_auth_http_method[] = { ngx_string("apop"), ngx_string("cram-md5"), ngx_string("external"), + ngx_string("xoauth2"), + ngx_string("oauthbearer"), ngx_string("none") }; @@ -677,6 +681,29 @@ ngx_mail_auth_http_process_headers(ngx_mail_session_t *s, continue; } + if (len == sizeof("Auth-Error-Sasl") - 1 + && ngx_strncasecmp(ctx->header_name_start, + (u_char *) "Auth-Error-Sasl", + sizeof("Auth-Error-Sasl") - 1) + == 0) + { + ctx->errsasl.len = ctx->header_end - ctx->header_start; + + ctx->errsasl.data = ngx_pnalloc(s->connection->pool, + ctx->errsasl.len); + if (ctx->errsasl.data == NULL) { + ngx_close_connection(ctx->peer.connection); + ngx_destroy_pool(ctx->pool); + ngx_mail_session_internal_server_error(s); + return; + } + + ngx_memcpy(ctx->errsasl.data, ctx->header_start, + ctx->errsasl.len); + + continue; + } + /* ignore other headers */ continue; @@ -716,13 +743,67 @@ ngx_mail_auth_http_process_headers(ngx_mail_session_t *s, p = ngx_cpymem(p, ctx->errmsg.data, ctx->errmsg.len); *p++ = CR; *p = LF; } + } - s->out = ctx->err; - timer = ctx->sleep; + if (ctx->errsasl.len) { - ngx_destroy_pool(ctx->pool); + if (s->auth_method == NGX_MAIL_AUTH_XOAUTH2 + || s->auth_method == NGX_MAIL_AUTH_OAUTHBEARER) + { + if (!ctx->err.len) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "auth http server %V returned SASL " + "error to auth success request", + ctx->peer.name); + ngx_destroy_pool(ctx->pool); + ngx_mail_session_internal_server_error(s); + return; + } - if (timer == 0) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "client login sasl failed: \"%V\"", &ctx->errsasl); + + if (s->protocol == NGX_MAIL_IMAP_PROTOCOL) { + size = ctx->errsasl.len + sizeof("+ " CRLF) - 1; + } else { + size = ctx->errsasl.len + sizeof("334 " CRLF) - 1; + } + + p = ngx_pnalloc(s->connection->pool, size); + if (p == NULL) { + ngx_destroy_pool(ctx->pool); + ngx_mail_session_internal_server_error(s); + return; + } + + if (s->protocol == NGX_MAIL_IMAP_PROTOCOL) { + *p++ = '+'; *p++ = ' '; + } else { + *p++ = '3'; *p++ = '3'; *p++ = '4'; *p++ = ' '; + } + p = ngx_cpymem(p, ctx->errsasl.data, ctx->errsasl.len); + *p++ = CR; *p++ = LF; + + ctx->errsasl.data = p - size; + ctx->errsasl.len = size; + + } else { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "auth http server %V returned SASL " + "error to non-SASL auth request", + ctx->peer.name); + ngx_destroy_pool(ctx->pool); + ngx_mail_session_internal_server_error(s); + return; + } + } + + if (ctx->err.len || ctx->errsasl.len) { + s->out = ctx->errsasl.len ? ctx->errsasl : ctx->err; + timer = ctx->sleep; + + if (timer == 0 && !ctx->errsasl.len) { + ngx_destroy_pool(ctx->pool); s->quit = 1; ngx_mail_send(s->connection->write); return; @@ -738,9 +819,8 @@ ngx_mail_auth_http_process_headers(ngx_mail_session_t *s, if (s->auth_wait) { timer = ctx->sleep; - ngx_destroy_pool(ctx->pool); - if (timer == 0) { + ngx_destroy_pool(ctx->pool); ngx_mail_auth_http_init(s); return; } @@ -854,29 +934,47 @@ ngx_mail_auth_http_process_headers(ngx_mail_session_t *s, } } - static void ngx_mail_auth_sleep_handler(ngx_event_t *rev) { ngx_connection_t *c; ngx_mail_session_t *s; ngx_mail_core_srv_conf_t *cscf; + ngx_mail_auth_http_ctx_t *ctx; ngx_log_debug0(NGX_LOG_DEBUG_MAIL, rev->log, 0, "mail auth sleep handler"); c = rev->data; s = c->data; + ctx = ngx_mail_get_module_ctx(s, ngx_mail_auth_http_module); + if (rev->timedout) { rev->timedout = 0; if (s->auth_wait) { s->auth_wait = 0; + ngx_destroy_pool(ctx->pool); ngx_mail_auth_http_init(s); return; } + if (ctx->errsasl.len) { + ngx_mail_send(c->write); + + if (c->destroyed) { + return; + } + + s->buffer->pos = s->buffer->start; + s->buffer->last = s->buffer->start; + s->connection->read->handler = ngx_mail_read_sasl_end_handler; + return; + } + + ngx_destroy_pool(ctx->pool); + cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module); rev->handler = cscf->protocol->auth_state; @@ -908,11 +1006,100 @@ ngx_mail_auth_sleep_handler(ngx_event_t *rev) if (rev->active) { if (ngx_handle_read_event(rev, 0) != NGX_OK) { + ngx_destroy_pool(ctx->pool); ngx_mail_close_connection(c); } } } +static void +ngx_mail_read_sasl_end_handler(ngx_event_t *rev) +{ + ssize_t n, len; + ngx_connection_t *c; + ngx_mail_session_t *s; + u_char *p; + ngx_mail_auth_http_ctx_t *ctx; + + c = rev->data; + s = c->data; + + ctx = ngx_mail_get_module_ctx(s, ngx_mail_auth_http_module); + + n = c->recv(c, s->buffer->last, s->buffer->end - s->buffer->last); + + if (n == NGX_ERROR || n == 0) { + goto error; + } + + if (n > 0) { + s->buffer->last += n; + } + + if (n == NGX_AGAIN) { + if (s->buffer->pos == s->buffer->last) { + goto error; + } + } + + if (ngx_handle_read_event(rev, 0) != NGX_OK) { + goto error; + } + + for (p = s->buffer->pos; p < s->buffer->last; p++) { + if (*p == LF && p > s->buffer->start && *(p-1) == CR) { + /* Three valid client lines. "*" on it's own is a cancel, see + * rfc3501 (imap), rfc4954 (smtp), rfc5034 (pop3), "AQ==" + * is client dummy response, see rfc7628, or empty line, + * see xoauth2 */ + len = p - s->buffer->start - 1; + if (len == 0) { + goto done; + } + if (len == 1 + && s->buffer->start[0] == '*') + { + goto done; + } + if (len == 4 + && s->buffer->start[0] == 'A' + && s->buffer->start[1] == 'Q' + && s->buffer->start[2] == '=' + && s->buffer->start[3] == '=') + { + goto done; + } + + goto error; + } + } + + s->buffer->pos = p; + return; + +done: + s->buffer->pos = s->buffer->start; + s->buffer->last = s->buffer->start; + + ctx->errsasl.len = 0; + rev->timedout = 1; + + s->out = ctx->err; + ngx_mail_send(c->write); + + if (c->destroyed) { + return; + } + + s->connection->read->handler = ngx_mail_auth_sleep_handler; + return; + +error: + ngx_destroy_pool(ctx->pool); + ngx_mail_close_connection(c); + return; +} + static ngx_int_t ngx_mail_auth_http_parse_header_line(ngx_mail_session_t *s, diff --git a/src/mail/ngx_mail_handler.c b/src/mail/ngx_mail_handler.c index 1167df3fb..e3add131b 100644 --- a/src/mail/ngx_mail_handler.c +++ b/src/mail/ngx_mail_handler.c @@ -752,6 +752,39 @@ ngx_mail_auth_external(ngx_mail_session_t *s, ngx_connection_t *c, return NGX_DONE; } +ngx_int_t +ngx_mail_auth_oauth(ngx_mail_session_t *s, ngx_connection_t *c, + ngx_uint_t n, ngx_uint_t auth_method) +{ + ngx_str_t *arg, oauth; + + arg = s->args.elts; + + ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth oauth: \"%V\" type %ui", &arg[n], auth_method); + + oauth.data = ngx_pnalloc(c->pool, ngx_base64_decoded_length(arg[n].len)); + if (oauth.data == NULL) { + return NGX_ERROR; + } + + if (ngx_decode_base64(&oauth, &arg[n]) != NGX_OK) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid base64 encoding in AUTH XOAUTH2/OAUTHBEARER command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + s->passwd.len = oauth.len; + s->passwd.data = oauth.data; + + ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth oauth: \"%V\"", &s->passwd); + + s->auth_method = auth_method; + + return NGX_DONE; +} + void ngx_mail_send(ngx_event_t *wev) diff --git a/src/mail/ngx_mail_imap_handler.c b/src/mail/ngx_mail_imap_handler.c index 291e87a4d..93e9e35fe 100644 --- a/src/mail/ngx_mail_imap_handler.c +++ b/src/mail/ngx_mail_imap_handler.c @@ -219,6 +219,14 @@ ngx_mail_imap_auth_state(ngx_event_t *rev) case ngx_imap_auth_external: rc = ngx_mail_auth_external(s, c, 0); break; + + case ngx_imap_auth_xoauth2: + rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_XOAUTH2); + break; + + case ngx_imap_auth_oauthbearer: + rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_OAUTHBEARER); + break; } } else if (rc == NGX_IMAP_NEXT) { @@ -416,6 +424,28 @@ ngx_mail_imap_authenticate(ngx_mail_session_t *s, ngx_connection_t *c) ngx_str_set(&s->out, imap_username); s->mail_state = ngx_imap_auth_external; + return NGX_OK; + + case NGX_MAIL_AUTH_XOAUTH2: + + if (!(iscf->auth_methods & NGX_MAIL_AUTH_XOAUTH2_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + ngx_str_set(&s->out, imap_plain_next); + s->mail_state = ngx_imap_auth_xoauth2; + + return NGX_OK; + + case NGX_MAIL_AUTH_OAUTHBEARER: + + if (!(iscf->auth_methods & NGX_MAIL_AUTH_OAUTHBEARER_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + ngx_str_set(&s->out, imap_plain_next); + s->mail_state = ngx_imap_auth_oauthbearer; + return NGX_OK; } diff --git a/src/mail/ngx_mail_imap_module.c b/src/mail/ngx_mail_imap_module.c index 02c684cd4..eff5e2c49 100644 --- a/src/mail/ngx_mail_imap_module.c +++ b/src/mail/ngx_mail_imap_module.c @@ -30,6 +30,8 @@ static ngx_conf_bitmask_t ngx_mail_imap_auth_methods[] = { { ngx_string("login"), NGX_MAIL_AUTH_LOGIN_ENABLED }, { ngx_string("cram-md5"), NGX_MAIL_AUTH_CRAM_MD5_ENABLED }, { ngx_string("external"), NGX_MAIL_AUTH_EXTERNAL_ENABLED }, + { ngx_string("xoauth2"), NGX_MAIL_AUTH_XOAUTH2_ENABLED }, + { ngx_string("oauthbearer"), NGX_MAIL_AUTH_OAUTHBEARER_ENABLED }, { ngx_null_string, 0 } }; @@ -40,6 +42,8 @@ static ngx_str_t ngx_mail_imap_auth_methods_names[] = { ngx_null_string, /* APOP */ ngx_string("AUTH=CRAM-MD5"), ngx_string("AUTH=EXTERNAL"), + ngx_string("AUTH=XOAUTH2"), + ngx_string("AUTH=OAUTHBEARER"), ngx_null_string /* NONE */ }; @@ -182,7 +186,7 @@ ngx_mail_imap_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) } for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { @@ -208,7 +212,7 @@ ngx_mail_imap_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) auth = p; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { diff --git a/src/mail/ngx_mail_parse.c b/src/mail/ngx_mail_parse.c index 4db1f18d3..3ad9032e2 100644 --- a/src/mail/ngx_mail_parse.c +++ b/src/mail/ngx_mail_parse.c @@ -946,6 +946,22 @@ ngx_mail_auth_parse(ngx_mail_session_t *s, ngx_connection_t *c) return NGX_MAIL_PARSE_INVALID_COMMAND; } + if (arg[0].len == 7) { + + if (ngx_strncasecmp(arg[0].data, (u_char *) "XOAUTH2", 7) == 0) { + + if (s->args.nelts == 1) { + return NGX_MAIL_AUTH_XOAUTH2; + } + + if (s->args.nelts == 2) { + return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_XOAUTH2); + } + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + if (arg[0].len == 8) { if (ngx_strncasecmp(arg[0].data, (u_char *) "CRAM-MD5", 8) == 0) { @@ -971,5 +987,21 @@ ngx_mail_auth_parse(ngx_mail_session_t *s, ngx_connection_t *c) return NGX_MAIL_PARSE_INVALID_COMMAND; } + if (arg[0].len == 11) { + + if (ngx_strncasecmp(arg[0].data, (u_char *) "OAUTHBEARER", 11) == 0) { + + if (s->args.nelts == 1) { + return NGX_MAIL_AUTH_OAUTHBEARER; + } + + if (s->args.nelts == 2) { + return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_OAUTHBEARER); + } + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + return NGX_MAIL_PARSE_INVALID_COMMAND; } diff --git a/src/mail/ngx_mail_pop3_handler.c b/src/mail/ngx_mail_pop3_handler.c index 226e7419b..8865aa637 100644 --- a/src/mail/ngx_mail_pop3_handler.c +++ b/src/mail/ngx_mail_pop3_handler.c @@ -259,6 +259,14 @@ ngx_mail_pop3_auth_state(ngx_event_t *rev) case ngx_pop3_auth_external: rc = ngx_mail_auth_external(s, c, 0); break; + + case ngx_pop3_auth_xoauth2: + rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_XOAUTH2); + break; + + case ngx_pop3_auth_oauthbearer: + rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_OAUTHBEARER); + break; } } @@ -537,6 +545,28 @@ ngx_mail_pop3_auth(ngx_mail_session_t *s, ngx_connection_t *c) ngx_str_set(&s->out, pop3_username); s->mail_state = ngx_pop3_auth_external; + return NGX_OK; + + case NGX_MAIL_AUTH_XOAUTH2: + + if (!(pscf->auth_methods & NGX_MAIL_AUTH_XOAUTH2_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + ngx_str_set(&s->out, pop3_next); + s->mail_state = ngx_pop3_auth_xoauth2; + + return NGX_OK; + + case NGX_MAIL_AUTH_OAUTHBEARER: + + if (!(pscf->auth_methods & NGX_MAIL_AUTH_OAUTHBEARER_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + ngx_str_set(&s->out, pop3_next); + s->mail_state = ngx_pop3_auth_oauthbearer; + return NGX_OK; } diff --git a/src/mail/ngx_mail_pop3_module.c b/src/mail/ngx_mail_pop3_module.c index a257b5a70..14b14fb24 100644 --- a/src/mail/ngx_mail_pop3_module.c +++ b/src/mail/ngx_mail_pop3_module.c @@ -30,6 +30,8 @@ static ngx_conf_bitmask_t ngx_mail_pop3_auth_methods[] = { { ngx_string("apop"), NGX_MAIL_AUTH_APOP_ENABLED }, { ngx_string("cram-md5"), NGX_MAIL_AUTH_CRAM_MD5_ENABLED }, { ngx_string("external"), NGX_MAIL_AUTH_EXTERNAL_ENABLED }, + { ngx_string("xoauth2"), NGX_MAIL_AUTH_XOAUTH2_ENABLED }, + { ngx_string("oauthbearer"), NGX_MAIL_AUTH_OAUTHBEARER_ENABLED }, { ngx_null_string, 0 } }; @@ -40,6 +42,8 @@ static ngx_str_t ngx_mail_pop3_auth_methods_names[] = { ngx_null_string, /* APOP */ ngx_string("CRAM-MD5"), ngx_string("EXTERNAL"), + ngx_string("XOAUTH2"), + ngx_string("OAUTHBEARER"), ngx_null_string /* NONE */ }; @@ -183,7 +187,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) size += sizeof("SASL") - 1 + sizeof(CRLF) - 1; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { @@ -214,7 +218,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) p = ngx_cpymem(p, "SASL", sizeof("SASL") - 1); for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { @@ -254,7 +258,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) + sizeof("." CRLF) - 1; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { @@ -279,7 +283,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) sizeof("+OK methods supported:" CRLF) - 1); for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { diff --git a/src/mail/ngx_mail_smtp_handler.c b/src/mail/ngx_mail_smtp_handler.c index e68ceedfd..885876fcb 100644 --- a/src/mail/ngx_mail_smtp_handler.c +++ b/src/mail/ngx_mail_smtp_handler.c @@ -547,6 +547,14 @@ ngx_mail_smtp_auth_state(ngx_event_t *rev) case ngx_smtp_auth_external: rc = ngx_mail_auth_external(s, c, 0); break; + + case ngx_smtp_auth_xoauth2: + rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_XOAUTH2); + break; + + case ngx_smtp_auth_oauthbearer: + rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_OAUTHBEARER); + break; } } @@ -729,6 +737,28 @@ ngx_mail_smtp_auth(ngx_mail_session_t *s, ngx_connection_t *c) ngx_str_set(&s->out, smtp_username); s->mail_state = ngx_smtp_auth_external; + return NGX_OK; + + case NGX_MAIL_AUTH_XOAUTH2: + + if (!(sscf->auth_methods & NGX_MAIL_AUTH_XOAUTH2_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + ngx_str_set(&s->out, smtp_next); + s->mail_state = ngx_smtp_auth_xoauth2; + + return NGX_OK; + + case NGX_MAIL_AUTH_OAUTHBEARER: + + if (!(sscf->auth_methods & NGX_MAIL_AUTH_OAUTHBEARER_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + ngx_str_set(&s->out, smtp_next); + s->mail_state = ngx_smtp_auth_oauthbearer; + return NGX_OK; } diff --git a/src/mail/ngx_mail_smtp_module.c b/src/mail/ngx_mail_smtp_module.c index 0e05fdc03..2f4a1f960 100644 --- a/src/mail/ngx_mail_smtp_module.c +++ b/src/mail/ngx_mail_smtp_module.c @@ -22,6 +22,8 @@ static ngx_conf_bitmask_t ngx_mail_smtp_auth_methods[] = { { ngx_string("login"), NGX_MAIL_AUTH_LOGIN_ENABLED }, { ngx_string("cram-md5"), NGX_MAIL_AUTH_CRAM_MD5_ENABLED }, { ngx_string("external"), NGX_MAIL_AUTH_EXTERNAL_ENABLED }, + { ngx_string("xoauth2"), NGX_MAIL_AUTH_XOAUTH2_ENABLED }, + { ngx_string("oauthbearer"), NGX_MAIL_AUTH_OAUTHBEARER_ENABLED }, { ngx_string("none"), NGX_MAIL_AUTH_NONE_ENABLED }, { ngx_null_string, 0 } }; @@ -33,6 +35,8 @@ static ngx_str_t ngx_mail_smtp_auth_methods_names[] = { ngx_null_string, /* APOP */ ngx_string("CRAM-MD5"), ngx_string("EXTERNAL"), + ngx_string("XOAUTH2"), + ngx_string("OAUTHBEARER"), ngx_null_string /* NONE */ }; @@ -210,7 +214,7 @@ ngx_mail_smtp_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) auth_enabled = 0; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { @@ -253,7 +257,7 @@ ngx_mail_smtp_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) *p++ = 'A'; *p++ = 'U'; *p++ = 'T'; *p++ = 'H'; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { From robm at fastmailteam.com Mon May 13 05:43:18 2024 From: robm at fastmailteam.com (Robert Mueller) Date: Mon, 13 May 2024 15:43:18 +1000 Subject: [nginx] Reset imap tag to empty after authentication attempt Message-ID: <3248a5e6-f931-4cca-a219-44d2451b837c@app.fastmail.com> # HG changeset patch # User Rob Mueller Reset imap tag to empty after authentication attempt We need to reset the imap tag to empty after an authentication attempt completes, otherwise if the next line parsed is incomplete with no tag (e.g. empty line) then we use the "tag" from the previous buffer which is now definitely wrong and has been partially overwritten with the most recently read data (e.g. CRLF). An example before this patch: S: * OK IMAP4 ready C: foobar login a b S: foobar NO Incorrect username or password. C: S: S: obar BAD invalid command Then with this patch: S: * OK IMAP4 ready C: foobar login a b S: foobar NO Incorrect username or password. C: S: * BAD invalid command --- src/mail/ngx_mail_auth_http_module.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/mail/ngx_mail_auth_http_module.c b/src/mail/ngx_mail_auth_http_module.c index 543d1c4eb..55280d66c 100644 --- a/src/mail/ngx_mail_auth_http_module.c +++ b/src/mail/ngx_mail_auth_http_module.c @@ -981,6 +981,7 @@ ngx_mail_auth_sleep_handler(ngx_event_t *rev) s->mail_state = 0; s->auth_method = NGX_MAIL_AUTH_PLAIN; + s->tag.len = 0; c->log->action = "in auth state"; From robm at fastmailteam.com Mon May 13 05:43:58 2024 From: robm at fastmailteam.com (Robert Mueller) Date: Mon, 13 May 2024 15:43:58 +1000 Subject: [nginx] Fix valgrind errors in mail tests Message-ID: <764acf27-4ee8-4a0a-a8b5-73e1c180014f@app.fastmail.com> # HG changeset patch # User Rob Mueller Fix valgrind errors in mail tests Applying this fix allows nginx to be run under valgrind and all mail_* tests to pass with no valgrand issues detected --- src/event/modules/ngx_epoll_module.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/event/modules/ngx_epoll_module.c b/src/event/modules/ngx_epoll_module.c index 98e3ce7c8..c89a56d95 100644 --- a/src/event/modules/ngx_epoll_module.c +++ b/src/event/modules/ngx_epoll_module.c @@ -474,6 +474,7 @@ ngx_epoll_test_rdhup(ngx_cycle_t *cycle) } ee.events = EPOLLET|EPOLLIN|EPOLLRDHUP; + ee.data.u64 = 0; if (epoll_ctl(ep, EPOLL_CTL_ADD, s[0], &ee) == -1) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, From robm at fastmailteam.com Mon May 13 06:38:56 2024 From: robm at fastmailteam.com (Robert Mueller) Date: Mon, 13 May 2024 16:38:56 +1000 Subject: [nginx-tests] Add test for WAIT auth response Message-ID: # HG changeset patch # User Rob Mueller Add test for WAIT auth response --- mail_auth_wait.t | 164 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 mail_auth_wait.t diff --git a/mail_auth_wait.t b/mail_auth_wait.t new file mode 100644 index 0000000..6132aaa --- /dev/null +++ b/mail_auth_wait.t @@ -0,0 +1,164 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for nginx mail module for WAIT response. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use MIME::Base64; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::IMAP; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +local $SIG{PIPE} = 'IGNORE'; + +my $t = Test::Nginx->new()->has(qw/mail imap http rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +mail { + proxy_pass_error_message on; + proxy_timeout 15s; + auth_http http://127.0.0.1:8080/mail/auth; + + server { + listen 127.0.0.1:8143; + protocol imap; + imap_auth plain cram-md5 external; + } +} + +http { + %%TEST_GLOBALS_HTTP%% + + map $upstream_http_count $reply { + # There won't be a 1, the waitforsocket(...) will get that + + # Each wait is 1 second, so wait 4 times, which should get us + # to after the sleep(3), but then after another sleep(2) we + # should have completed with a success + 2 WAIT; + 3 WAIT; + 4 WAIT; + 5 WAIT; + 6 OK; + default ERROR; + } + + log_format test "reply=$reply"; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + access_log %%TESTDIR%%/auth.log test; + + location = /mail/auth { + add_header Auth-Status $reply; + add_header Auth-Server 127.0.0.1; + add_header Auth-Port %%PORT_8144%%; + add_header Auth-Pass ""; + add_header Auth-Wait 1; + proxy_pass http://127.0.0.1:8083; + } + } +} + +EOF + +$t->run_daemon(\&Test::Nginx::IMAP::imap_test_daemon); +$t->run_daemon(\&http_daemon, port(8083)); +$t->run()->plan(4); + +$t->waitforsocket('127.0.0.1:' . port(8144)); +$t->waitforsocket('127.0.0.1:' . port(8083)); + +############################################################################### + +# WAIT response + +my $s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('a01 LOGIN test at example.com wait'); +note("sent LOGIN, sleeping"); + +sleep(3); + +my $f = $t->read_file('auth.log'); +my @waits = $f =~ /^reply=WAIT/mg; +ok(@waits >= 2, "found multiple WAIT responses in log"); + +my @ready = $s->can_read(0.1); +is(scalar @ready, 0, "nothing to read while waiting"); + +sleep(2); + + at ready = $s->can_read(0); +is(scalar @ready, 1, "ready for reading"); + +$s->ok('login success after waiting'); + +############################################################################### + +sub http_daemon { + my ($port) = @_; + + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => $port, + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + my $count = 0; + while (my $client = $server->accept()) { + $count++; + $client->autoflush(1); + + my $headers = ''; + my $uri = ''; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + + next if $headers eq ''; + + Test::Nginx::log_core('||', "$port: response, 204"); + print $client < # HG changeset patch # User Rob Mueller Add test for mail proxy timeout --- mail_proxy_timeout.t | 124 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 mail_proxy_timeout.t diff --git a/mail_proxy_timeout.t b/mail_proxy_timeout.t new file mode 100644 index 0000000..f092d4b --- /dev/null +++ b/mail_proxy_timeout.t @@ -0,0 +1,124 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for nginx mail module for WAIT response. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use MIME::Base64; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::IMAP; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +local $SIG{PIPE} = 'IGNORE'; + +my $t = Test::Nginx->new()->has(qw/mail imap http rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +mail { + proxy_pass_error_message on; + timeout 2s; + proxy_timeout 2s; + auth_http http://127.0.0.1:8080/mail/auth; + + server { + listen 127.0.0.1:8143; + protocol imap; + imap_auth plain cram-md5 external; + } +} + +http { + %%TEST_GLOBALS_HTTP%% + + map $http_auth_pass $reply { + secret OK; + default ERROR; + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location = /mail/auth { + add_header Auth-Status $reply; + add_header Auth-Server 127.0.0.1; + add_header Auth-Port %%PORT_8144%%; + add_header Auth-Pass ""; + add_header Auth-Wait 1; + return 204; + } + } +} + +EOF + +$t->run_daemon(\&Test::Nginx::IMAP::imap_test_daemon); +$t->run()->plan(8); + +$t->waitforsocket('127.0.0.1:' . port(8144)); + +############################################################################### + +# check proxy timeout + +my $s = Test::Nginx::IMAP->new(); +$s->read(); + +# Each of these will wait 1 second before response + +$s->send('a01 LOGIN test at example.com bad'); +$s->check(qr/^a01 NO/, 'login with bad password'); + +$s->send('a01 LOGIN test at example.com bad'); +$s->check(qr/^a01 NO/, 'login with bad password'); + +sleep(1); + +# Total timeout is 2 seconds, so connection should have been closed + +my @ready = $s->can_read(0); +is(scalar @ready, 1, "ready for reading"); +ok($s->eof(), "session closed"); + + +$s = Test::Nginx::IMAP->new(); +$s->read(); + +$s->send('a01 LOGIN test at example.com secret'); +$s->ok('login'); + + at ready = $s->can_read(0.1); +is(scalar @ready, 0, "nothing to read after login"); + +sleep(3); + +# Total timeout is 2 seconds, so connection should have been closed + + at ready = $s->can_read(0); +is(scalar @ready, 1, "ready for reading"); +ok($s->eof(), "session closed"); + +############################################################################### From robm at fastmailteam.com Mon May 13 06:43:00 2024 From: robm at fastmailteam.com (Robert Mueller) Date: Mon, 13 May 2024 16:43:00 +1000 Subject: [nginx-tests] Add valgrind support to nginx test suite Message-ID: # HG changeset patch # User Rob Mueller Add valgrind support to nginx test suite Setting TEST_NGINX_VALGRIND=1 will cause the test suite to run nginx under valgrind and check at exit that there's no errors present --- lib/Test/Nginx.pm | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm index 7b7bdc5..f363349 100644 --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -86,7 +86,12 @@ sub DESTROY { local $Test::Nginx::TODO; my $errors = $self->read_file('error.log'); $errors = join "\n", $errors =~ /.+Sanitizer.+/gm; - Test::More::is($errors, '', 'no sanitizer errors'); + my $extra = ""; + if ($ENV{TEST_NGINX_VALGRIND}) { + $extra = "/valgrind"; + $errors .= $self->read_file('valgrind.log'); + } + Test::More::is($errors, '', "no sanitizer${extra} errors"); } if ($ENV{TEST_NGINX_CATLOG}) { @@ -398,7 +403,11 @@ sub run(;$) { my @globals = $self->{_test_globals} ? () : ('-g', "pid $testdir/nginx.pid; " . "error_log $testdir/error.log debug;"); - exec($NGINX, '-p', "$testdir/", '-c', 'nginx.conf', + my @cmd = ($NGINX); + if ($ENV{TEST_NGINX_VALGRIND}) { + unshift @cmd, 'valgrind', '-q', "--log-file=$testdir/valgrind.log"; + } + exec(@cmd, '-p', "$testdir/", '-c', 'nginx.conf', '-e', 'error.log', @globals) or die "Unable to exec(): $!\n"; } From robm at fastmailteam.com Mon May 13 06:44:24 2024 From: robm at fastmailteam.com (Robert Mueller) Date: Mon, 13 May 2024 16:44:24 +1000 Subject: [nginx-tests] Add tests for OAUTHBEARER and Auth-Error-Sasl response Message-ID: <327d1ce2-094d-4af3-a6d3-d86f176b49e7@app.fastmail.com> # HG changeset patch # User Rob Mueller Add tests for OAUTHBEARER and Auth-Error-Sasl response --- mail_auth_sasl.t | 278 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 mail_auth_sasl.t diff --git a/mail_auth_sasl.t b/mail_auth_sasl.t new file mode 100644 index 0000000..1e71b47 --- /dev/null +++ b/mail_auth_sasl.t @@ -0,0 +1,278 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for nginx mail imap module. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use MIME::Base64; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::IMAP; +use Test::Nginx::SMTP; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +local $SIG{PIPE} = 'IGNORE'; + +my $t = Test::Nginx->new()->has(qw/mail imap smtp http rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +mail { + proxy_pass_error_message on; + proxy_timeout 15s; + timeout 2s; + auth_http http://127.0.0.1:8080/mail/auth; + + server { + listen 127.0.0.1:8143; + protocol imap; + imap_auth plain cram-md5 external oauthbearer; + } + server { + listen 127.0.0.1:8025; + protocol smtp; + smtp_auth plain external oauthbearer; + } +} + +http { + %%TEST_GLOBALS_HTTP%% + + map $http_auth_protocol $proxy_port { + imap %%PORT_8144%%; + smtp %%PORT_8026%%; + } + + map $http_auth_pass $reply { + ~secretok OK; + default auth-failed; + } + map $http_auth_pass $passw { + ~secretok secret; + default ""; + } + map $http_auth_pass $sasl { + ~saslfail "eyJzY2hlbWVzIjoiQmVhcmVyIiwic3RhdHVzIjoiNDAwIn0="; + default ""; + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + + error_log %%TESTDIR%%/error.log info; + + location = /mail/auth { + add_header Auth-Status $reply; + add_header Auth-Server 127.0.0.1; + add_header Auth-Port $proxy_port; + add_header Auth-Pass $passw; + add_header Auth-Wait 1; + add_header Auth-Error-Sasl $sasl; + return 204; + } + } +} + +EOF + +$t->run_daemon(\&Test::Nginx::IMAP::imap_test_daemon); +$t->run_daemon(\&Test::Nginx::SMTP::smtp_test_daemon); +$t->run()->plan(31); + +$t->waitforsocket('127.0.0.1:' . port(8144)); +$t->waitforsocket('127.0.0.1:' . port(8026)); + +############################################################################### + +# auth oauthbearer +# See https://datatracker.ietf.org/doc/html/rfc7628 for some examples + +# SMTP +{ + +# success, without IR + +my $s = Test::Nginx::SMTP->new(); +$s->read(); +$s->send('EHLO example.com'); +$s->read(); +$s->send('AUTH OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer secretok\001\001", '')); +$s->authok('oauthbearer success'); + +# success, with IR + +$s = Test::Nginx::SMTP->new(); +$s->read(); +$s->send('EHLO example.com'); +$s->read(); +$s->send('AUTH OAUTHBEARER'); +$s->check(qr/^334 /, 'auth oauthbearer challenge'); +$s->send(encode_base64("n,user=test\@example.com,\001auth=Bearer secretok\001\001", '')); +$s->authok('oauthbearer success'); + +# fail, sasl failure method, end via sasl 'AQ==' response + +$s = Test::Nginx::SMTP->new(); +$s->read(); +$s->send('EHLO example.com'); +$s->read(); +$s->send('AUTH OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer saslfail\001\001", '')); +$s->check(qr/^334 /, 'auth oauthbearer with bad token'); +$s->send('AQ=='); +$s->check(qr/^535 /, 'got smtp auth failure response after sasl end line'); + +# fail, sasl failure method, end via empty line + +$s = Test::Nginx::SMTP->new(); +$s->read(); +$s->send('EHLO example.com'); +$s->read(); +$s->send('AUTH OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer saslfail\001\001", '')); +$s->check(qr/^334 /, 'auth oauthbearer with bad token'); +$s->send(''); +$s->check(qr/^535 /, 'got smtp auth failure response after sasl end line'); + +# fail, sasl failure method, invalid client response causes dropped connection + +$s = Test::Nginx::SMTP->new(); +$s->read(); +$s->send('EHLO example.com'); +$s->read(); +$s->send('AUTH OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer saslfail\001\001", '')); +$s->check(qr/^334 /, 'auth oauthbearer with bad token'); +$s->send('foo'); +ok($s->eof(), "got disconnect after invalid client line"); + +} + +# IMAP +{ + +# success, with IR + +my $s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer secretok\001\001", '')); +$s->ok('auth bearer success in IR'); + +# success, without IR + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER'); +$s->check(qr/\+ /, 'auth bearer challenge'); +$s->send(encode_base64("n,user=test\@example.com,\001auth=Bearer secretok\001\001", '')); +$s->ok('auth bearer success'); + +# fail, standard non-sasl failure method + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer bad\001\001", '')); +$s->check(qr/^1 NO auth-failed/, 'imap auth failure response'); + +sleep(3); + +my @ready = $s->can_read(0); +is(scalar @ready, 1, "ready for reading"); +ok($s->eof(), "session closed"); + +# fail, sasl failure method, end via empty line + +$s = Test::Nginx::IMAP->new(); +$s->read(); +my $start = time; +$s->send('1 AUTHENTICATE OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer saslfail\001\001", '')); +$s->check(qr/^\+ eyJzY2hlbWVzIjoiQmVhcmVyIiwic3RhdHVzIjoiNDAwIn0=/, 'got imap sasl failure response'); +my $wait_time = time - $start; +ok($wait_time >= 1, 'had to wait at least 1 second to get error line'); +$s->send(''); +$s->check(qr/^1 NO auth-failed/, 'got imap auth failure response after empty client line'); + +# fail, sasl failure method, dropped connection +# (closed connection makes ngx_mail_send destroy pool, make +# sure timeout sleep handler doesn't try and use it) + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer saslfail\001\001", '')); +sleep(1); +$s->send(''); +$s = undef; + +sleep(2); + +# fail, sasl failure method, end via sasl 'AQ==' response + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer saslfail\001\001", '')); +$s->check(qr/^\+ eyJzY2hlbWVzIjoiQmVhcmVyIiwic3RhdHVzIjoiNDAwIn0=/, 'got imap sasl failure response'); +$s->send('AQ=='); +$s->check(qr/^1 NO auth-failed/, 'got imap auth failure response after sasl end line'); + +# fail, sasl failure method, invalid client response causes dropped connection + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer saslfail\001\001", '')); +$s->check(qr/^\+ eyJzY2hlbWVzIjoiQmVhcmVyIiwic3RhdHVzIjoiNDAwIn0=/, 'got imap sasl failure response'); +$s->send('foo'); +ok($s->eof(), "got disconnect after invalid client line"); + +# fail, sasl failure method, but Auth-Status: OK is disconnect with internal error + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer secretok_saslfail\001\001", '')); +$s->check(qr/^\* BAD internal server error/, 'got internal error response'); +ok($s->eof(), "sasl vs error mismatch causes disconnect"); + +my $e = $t->read_file('error.log'); +like($e, qr/returned SASL error to auth success/, "error log documents mismatch error"); + +# fail, sasl failure method, multiple attempts, then success + +$s = Test::Nginx::IMAP->new(); +$s->read(); + +$s->send('1 AUTHENTICATE OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer saslfail\001\001", '')); +$s->check(qr/^\+ eyJzY2hlbWVzIjoiQmVhcmVyIiwic3RhdHVzIjoiNDAwIn0=/, 'got imap sasl failure response'); +$s->send(''); +$s->check(qr/^1 NO auth-failed/, 'got imap auth failure response after empty client line'); + +$s->send('1 AUTHENTICATE OAUTHBEARER ' . encode_base64("n,user=test\@example.com,\001auth=Bearer saslfail\001\001", '')); +$s->check(qr/^\+ eyJzY2hlbWVzIjoiQmVhcmVyIiwic3RhdHVzIjoiNDAwIn0=/, 'got imap sasl failure response'); +$s->send(''); +$s->check(qr/^1 NO auth-failed/, 'got imap auth failure response after empty client line'); + +$s->send('1 AUTHENTICATE OAUTHBEARER'); +$s->check(qr/\+ /, 'auth bearer challenge'); +$s->send(encode_base64("n,user=test\@example.com,\001auth=Bearer secretok\001\001", '')); +$s->ok('auth bearer success'); + +} + +############################################################################### From mdounin at mdounin.ru Mon May 13 23:27:56 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 14 May 2024 02:27:56 +0300 Subject: [PATCH] Documented "pid off" Message-ID: <4e037b73ea79b64422b4.1715642876@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1715641805 -10800 # Tue May 14 02:10:05 2024 +0300 # Node ID 4e037b73ea79b64422b453a7f5ccff71382e9670 # Parent d099a7bc5d226cabdefe8994374d0bbfd84ad07b Documented "pid off". diff --git a/xml/en/docs/ngx_core_module.xml b/xml/en/docs/ngx_core_module.xml --- a/xml/en/docs/ngx_core_module.xml +++ b/xml/en/docs/ngx_core_module.xml @@ -10,7 +10,7 @@ + rev="28">
@@ -403,12 +403,13 @@ the JIT support is enabled via the -file +file | off logs/nginx.pid main Defines a file that will store the process ID of the main process. +The off parameter (1.27.0) disables writing of a PID file. diff --git a/xml/ru/docs/ngx_core_module.xml b/xml/ru/docs/ngx_core_module.xml --- a/xml/ru/docs/ngx_core_module.xml +++ b/xml/ru/docs/ngx_core_module.xml @@ -10,7 +10,7 @@ + rev="28">
@@ -401,12 +401,13 @@ load_module modules/ngx_mail_module.so; -???? +???? | off logs/nginx.pid main ?????? ????, ? ??????? ????? ????????? ????? (PID) ???????? ????????. +???????? off (1.27.0) ????????? ?????? PID-?????. From mdounin at mdounin.ru Tue May 14 01:40:24 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Tue, 14 May 2024 04:40:24 +0300 Subject: freenginx-1.27.0 changes draft Message-ID: Hello! Below are changes draft for freenginx 1.27.0. Comments are welcome. Changes with freenginx 1.27.0 14 May 2024 *) Feature: updated descriptions of HTTP status codes. Thanks to Michiel W. Beijen. *) Change: now, if an error occurs during reading a request body, the request body is automatically discarded, and for complex error processing, such as proxying, it is no longer needed to explicitly disable passing of the request body to the proxied server. *) Change: the logging level of the "SSL alert number N" and "invalid alert" SSL errors has been lowered from "crit" to "info". *) Change: now freenginx always returns an error if a header name is not followed by a colon. Thanks to Maksim Yevmenkin. *) Feature: the "off" parameter of the "pid" directive. *) Feature: now during reconfiguration no attempt to recreate the PID file is made if the name in the "pid" directive was changed, but points to the same file via symlinks. *) Workaround: "PID file ... not readable (yet?) after start" and "Failed to parse PID from file..." errors might appear when starting with systemd. *) Bugfix: no error was written to the error log when a timeout occurred during reading a request body. *) Bugfix: redirecting errors with code 413 with the "error_page" directive worked incorrectly when using HTTP/2 and HTTP/3. *) Bugfix: freenginx could not be built on NetBSD 10.0. *) Bugfix: in HTTP/3. ????????? ? freenginx 1.27.0 14.05.2024 *) ??????????: ????????? ????????? ???????? ????? ???????. ??????? Michiel W. Beijen. *) ?????????: ??????, ???? ??? ?????? ???? ??????? ?????????? ??????, ???? ??????? ????????????? ?????????????, ? ??? ??????? ????????? ??????, ? ????????? ??? ????????????? ??????, ?? ????????? ???? ????????? ???????? ???? ??????? ?? ???????????? ??????. *) ?????????: ??????? ???????????? ?????? SSL "SSL alert number N" ? "invalid alert" ??????? ? ?????? crit ?? info. *) ?????????: ?????? freenginx ?????? ?????????? ??????, ???? ?? ?????? ????????? ?? ??????? ?????????. ??????? ??????? ?????????. *) ??????????: ???????? off ? ????????? pid. *) ??????????: ?????? ??? ???????????????? ?? ???????? ??????? ??????????? PID-????, ???? ??? ? ????????? pid ??????????, ?? ????????? ?? ??? ?? ???? ????? ????????. *) ?????????: ??? ??????? ??? systemd ????? ????????? ?????? "PID file ... not readable (yet?) after start" ? "Failed to parse PID from file...". *) ???????????: ???? ?? ????? ?????? ???? ??????? ?????????? ???????, ?? ???? ?? ?????????? ? ??? ??????. *) ???????????: ??????????????? ?????? 413 ? ??????? ????????? error_page ???????? ??????????? ??? ????????????? HTTP/2 ? HTTP/3. *) ???????????: freenginx ?? ????????? ?? NetBSD 10.0. *) ???????????: ? HTTP/3. -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Tue May 14 14:53:32 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 14 May 2024 17:53:32 +0300 Subject: [nginx] freenginx-1.27.0-RELEASE Message-ID: details: http://freenginx.org/hg/nginx/rev/8c4e2b7de093 branches: changeset: 9271:8c4e2b7de093 user: Maxim Dounin date: Tue May 14 17:47:43 2024 +0300 description: freenginx-1.27.0-RELEASE diffstat: docs/xml/nginx/changes.xml | 132 +++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 132 insertions(+), 0 deletions(-) diffs (142 lines): diff --git a/docs/xml/nginx/changes.xml b/docs/xml/nginx/changes.xml --- a/docs/xml/nginx/changes.xml +++ b/docs/xml/nginx/changes.xml @@ -7,6 +7,138 @@
+ + + + +????????? ????????? ???????? ????? ???????.
+??????? Michiel W. Beijen. +
+ +updated descriptions of HTTP status codes.
+Thanks to Michiel W. Beijen. +
+
+ + + +??????, ???? ??? ?????? ???? ??????? ?????????? ??????, +???? ??????? ????????????? ?????????????, +? ??? ??????? ????????? ??????, ? ????????? ??? ????????????? ??????, +?? ????????? ???? ????????? +???????? ???? ??????? ?? ???????????? ??????. + + +now, if an error occurs during reading a request body, +the request body is automatically discarded, +and for complex error processing, such as proxying, +it is no longer needed to explicitly disable +passing of the request body to the proxied server. + + + + + +??????? ???????????? ?????? SSL "SSL alert number N" ? "invalid alert" +??????? ? ?????? crit ?? info. + + +the logging level of the "SSL alert number N" and "invalid alert" SSL errors +has been lowered from "crit" to "info". + + + + + +?????? freenginx ?????? ?????????? ??????, +???? ?? ?????? ????????? ?? ??????? ?????????.
+??????? ??????? ?????????. +
+ +now freenginx always returns an error +if a header name is not followed by a colon.
+Thanks to Maksim Yevmenkin. +
+
+ + + +???????? off ? ????????? pid. + + +the "off" parameter of the "pid" directive. + + + + + +?????? ??? ???????????????? ?? ???????? ??????? ??????????? PID-????, +???? ??? ? ????????? pid ??????????, +?? ????????? ?? ??? ?? ???? ????? ????????. + + +now during reconfiguration no attempt to recreate the PID file is made +if the name in the "pid" directive was changed, +but points to the same file via symlinks. + + + + + +??? ??????? ??? systemd ????? ????????? ?????? +"PID file ... not readable (yet?) after start" ? +"Failed to parse PID from file...". + + +"PID file ... not readable (yet?) after start" and +"Failed to parse PID from file..." errors +might appear when starting with systemd. + + + + + +???? ?? ????? ?????? ???? ??????? ?????????? ???????, +?? ???? ?? ?????????? ? ??? ??????. + + +no error was written to the error log +when a timeout occurred during reading a request body. + + + + + +??????????????? ?????? 413 ? ??????? ????????? error_page +???????? ??????????? ??? ????????????? HTTP/2 ? HTTP/3. + + +redirecting errors with code 413 with the "error_page" directive +worked incorrectly when using HTTP/2 and HTTP/3. + + + + + +freenginx ?? ????????? ?? NetBSD 10.0. + + +freenginx could not be built on NetBSD 10.0. + + + + + +? HTTP/3. + + +in HTTP/3. + + + +
+ + From mdounin at mdounin.ru Tue May 14 14:53:32 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 14 May 2024 17:53:32 +0300 Subject: [nginx] release-1.27.0 tag Message-ID: details: http://freenginx.org/hg/nginx/rev/4cfd64a8330d branches: changeset: 9272:4cfd64a8330d user: Maxim Dounin date: Tue May 14 17:47:44 2024 +0300 description: release-1.27.0 tag diffstat: .hgtags | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diffs (8 lines): diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -478,3 +478,4 @@ 1d839f05409d1a50d0f15a2bf36547001f99ae40 294a3d07234f8f65d7b0e0b0e2c5b05c12c5da0a release-1.25.3 ab948bfa042d7a7b20c3e730d7e9675cc172324f release-1.25.4 2956b59565c91baa79d13d6411f2404614c0134e release-1.25.5 +8c4e2b7de093d357b2f462399d6d395b899ffe76 release-1.27.0 From mdounin at mdounin.ru Tue May 14 15:02:03 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 14 May 2024 18:02:03 +0300 Subject: [nginx-site] Documented "pid off". Message-ID: details: http://freenginx.org/hg/nginx-site/rev/eb5950986b11 branches: changeset: 3081:eb5950986b11 user: Maxim Dounin date: Tue May 14 05:30:46 2024 +0300 description: Documented "pid off". diffstat: xml/en/docs/ngx_core_module.xml | 5 +++-- xml/ru/docs/ngx_core_module.xml | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diffs (54 lines): diff --git a/xml/en/docs/ngx_core_module.xml b/xml/en/docs/ngx_core_module.xml --- a/xml/en/docs/ngx_core_module.xml +++ b/xml/en/docs/ngx_core_module.xml @@ -10,7 +10,7 @@ + rev="28">
@@ -403,12 +403,13 @@ the JIT support is enabled via the -file +file | off logs/nginx.pid main Defines a file that will store the process ID of the main process. +The off parameter (1.27.0) disables writing a PID file. diff --git a/xml/ru/docs/ngx_core_module.xml b/xml/ru/docs/ngx_core_module.xml --- a/xml/ru/docs/ngx_core_module.xml +++ b/xml/ru/docs/ngx_core_module.xml @@ -10,7 +10,7 @@ + rev="28">
@@ -401,12 +401,13 @@ load_module modules/ngx_mail_module.so; -???? +???? | off logs/nginx.pid main ?????? ????, ? ??????? ????? ????????? ????? (PID) ???????? ????????. +???????? off (1.27.0) ????????? ?????? PID-?????. From mdounin at mdounin.ru Tue May 14 15:02:04 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 14 May 2024 18:02:04 +0300 Subject: [nginx-site] freenginx-1.27.0 Message-ID: details: http://freenginx.org/hg/nginx-site/rev/796fbd13ae36 branches: changeset: 3082:796fbd13ae36 user: Maxim Dounin date: Tue May 14 17:59:30 2024 +0300 description: freenginx-1.27.0 diffstat: text/en/CHANGES | 38 ++++++++++++++++++++++++++++++++++++++ text/ru/CHANGES.ru | 38 ++++++++++++++++++++++++++++++++++++++ xml/index.xml | 10 ++++++++++ xml/versions.xml | 2 +- 4 files changed, 87 insertions(+), 1 deletions(-) diffs (124 lines): diff --git a/text/en/CHANGES b/text/en/CHANGES --- a/text/en/CHANGES +++ b/text/en/CHANGES @@ -1,4 +1,42 @@ +Changes with freenginx 1.27.0 14 May 2024 + + *) Feature: updated descriptions of HTTP status codes. + Thanks to Michiel W. Beijen. + + *) Change: now, if an error occurs during reading a request body, the + request body is automatically discarded, and for complex error + processing, such as proxying, it is no longer needed to explicitly + disable passing of the request body to the proxied server. + + *) Change: the logging level of the "SSL alert number N" and "invalid + alert" SSL errors has been lowered from "crit" to "info". + + *) Change: now freenginx always returns an error if a header name is not + followed by a colon. + Thanks to Maksim Yevmenkin. + + *) Feature: the "off" parameter of the "pid" directive. + + *) Feature: now during reconfiguration no attempt to recreate the PID + file is made if the name in the "pid" directive was changed, but + points to the same file via symlinks. + + *) Workaround: "PID file ... not readable (yet?) after start" and + "Failed to parse PID from file..." errors might appear when starting + with systemd. + + *) Bugfix: no error was written to the error log when a timeout occurred + during reading a request body. + + *) Bugfix: redirecting errors with code 413 with the "error_page" + directive worked incorrectly when using HTTP/2 and HTTP/3. + + *) Bugfix: freenginx could not be built on NetBSD 10.0. + + *) Bugfix: in HTTP/3. + + Changes with freenginx 1.25.5 02 Apr 2024 *) Change: MIME type for the "bmp" extension has been changed to diff --git a/text/ru/CHANGES.ru b/text/ru/CHANGES.ru --- a/text/ru/CHANGES.ru +++ b/text/ru/CHANGES.ru @@ -1,4 +1,42 @@ +????????? ? freenginx 1.27.0 14.05.2024 + + *) ??????????: ????????? ????????? ???????? ????? ???????. + ??????? Michiel W. Beijen. + + *) ?????????: ??????, ???? ??? ?????? ???? ??????? ?????????? ??????, + ???? ??????? ????????????? ?????????????, ? ??? ??????? ????????? + ??????, ? ????????? ??? ????????????? ??????, ?? ????????? ???? + ????????? ???????? ???? ??????? ?? ???????????? ??????. + + *) ?????????: ??????? ???????????? ?????? SSL "SSL alert number N" ? + "invalid alert" ??????? ? ?????? crit ?? info. + + *) ?????????: ?????? freenginx ?????? ?????????? ??????, ???? ?? ?????? + ????????? ?? ??????? ?????????. + ??????? ??????? ?????????. + + *) ??????????: ???????? off ? ????????? pid. + + *) ??????????: ?????? ??? ???????????????? ?? ???????? ??????? + ??????????? PID-????, ???? ??? ? ????????? pid ??????????, ?? + ????????? ?? ??? ?? ???? ????? ????????. + + *) ?????????: ??? ??????? ??? systemd ????? ????????? ?????? "PID file + ... not readable (yet?) after start" ? "Failed to parse PID from + file...". + + *) ???????????: ???? ?? ????? ?????? ???? ??????? ?????????? ???????, ?? + ???? ?? ?????????? ? ??? ??????. + + *) ???????????: ??????????????? ?????? 413 ? ??????? ????????? + error_page ???????? ??????????? ??? ????????????? HTTP/2 ? HTTP/3. + + *) ???????????: freenginx ?? ????????? ?? NetBSD 10.0. + + *) ???????????: ? HTTP/3. + + ????????? ? freenginx 1.25.5 02.04.2024 *) ?????????: MIME-??? ??? ?????????? bmp ??????? ?? "image/bmp", ??? diff --git a/xml/index.xml b/xml/index.xml --- a/xml/index.xml +++ b/xml/index.xml @@ -8,6 +8,16 @@ + + +freenginx-1.27.0 +mainline version has been released, +featuring better handling of errors during reading request bodies +and PID file +writing improvements. + + + freenginx-1.26.0 diff --git a/xml/versions.xml b/xml/versions.xml --- a/xml/versions.xml +++ b/xml/versions.xml @@ -9,7 +9,7 @@ - + From kasei at kasei.im Wed May 15 03:46:29 2024 From: kasei at kasei.im (Kasei Wang) Date: Wed, 15 May 2024 11:46:29 +0800 Subject: [PATCH]HTTP/2 connection not properly closing during graceful shutdown Message-ID: <0255e7345ffdebf8081563334ed7c931718d2c19.camel@kasei.im> Hello, I found that there is a slight probability of HTTP/2 connections not properly closing during graceful shutdown, leading to worker processes in shutting down state remaining stuck for an extended period. After investigation, the issue appears to stem from the following: 1. worker processes in shutting down state use ngx_close_idle_connections to close all idle connections, including HTTP/2 connections. 2. For HTTP/2 connections, c->idle is set to true in ngx_http_v2_init. According to the explanation in , GOAWAY should be sent to all HTTP/2 connections. 3. There might be a time gap between ngx_event_accept and ngx_http_v2_init. For TLS connections, ngx_http_v2_init will be executed after ALPN received, and for plaintext http2 connections, ngx_http_v2_init will be executed after parsing the http2 preface. If ngx_close_idle_connections is executed between ngx_event_accept and ngx_http_v2_init, there's a possibility that c->idle of some connections is set to true AFTER ngx_close_idle_connections, causing those connections to not enter the GOAWAY process and leading to the aforementioned problem. To verify this, I've written a simple HTTP/2 client. This program will wait 15 seconds after TCP connection establishment before starting to send data. The purpose of sleep is to to raise the probability of encountering the issue. You can reproduce the problem by executing "nginx -s reload" during this 15-second wait. If you're interested, you can try my test program () to reproduce the issue. The following patch would call ngx_http_v2_finalize_connection to close http2 connections which is initialized after ngx_close_idle_connections. And here are some previous discussion on the another maillist: Please confirm if this issue exists, review my analysis and the patch if possible. Thank you very much. # HG changeset patch # User Kasei Wang # Date 1715744317 -28800 # ? ? ?Wed May 15 11:38:37 2024 +0800 # Node ID 7f18358cc012039f6bb4e502a6f39a7bd50d669b # Parent ?4cfd64a8330dd5bacd839796732fd8c49d87e86e HTTP/2: close http2 connections initialized during graceful shutdown. In some rare cases, a HTTP/2 connections can be initialized during a graceful shutdown. Now close such an connection to avoid unexcepted delays in the graceful shutdown. diff -r 4cfd64a8330d -r 7f18358cc012 src/http/v2/ngx_http_v2.c --- a/src/http/v2/ngx_http_v2.c Tue May 14 17:47:44 2024 +0300 +++ b/src/http/v2/ngx_http_v2.c Wed May 15 11:38:37 2024 +0800 @@ -304,6 +304,11 @@ ? ? ?c->idle = 1; ? ? ?ngx_reusable_connection(c, 0); + ? ?if (ngx_exiting) { + ? ? ? ?ngx_http_v2_finalize_connection(h2c, NGX_HTTP_V2_NO_ERROR); + ? ? ? ?return; + ? ?} + ? ? ?if (c->buffer) { ? ? ? ? ?p = c->buffer->pos; ? ? ? ? ?end = c->buffer->last; From mdounin at mdounin.ru Wed May 15 06:06:06 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Wed, 15 May 2024 09:06:06 +0300 Subject: [nginx] Fix valgrind errors in mail tests In-Reply-To: <764acf27-4ee8-4a0a-a8b5-73e1c180014f@app.fastmail.com> References: <764acf27-4ee8-4a0a-a8b5-73e1c180014f@app.fastmail.com> Message-ID: Hello! On Mon, May 13, 2024 at 03:43:58PM +1000, Robert Mueller wrote: > # HG changeset patch > # User Rob Mueller > Fix valgrind errors in mail tests > > Applying this fix allows nginx to be run under valgrind and all > mail_* tests to pass with no valgrand issues detected > --- > src/event/modules/ngx_epoll_module.c | 1 + > 1 file changed, 1 insertion(+) > > diff --git a/src/event/modules/ngx_epoll_module.c b/src/event/modules/ngx_epoll_module.c > index 98e3ce7c8..c89a56d95 100644 > --- a/src/event/modules/ngx_epoll_module.c > +++ b/src/event/modules/ngx_epoll_module.c > @@ -474,6 +474,7 @@ ngx_epoll_test_rdhup(ngx_cycle_t *cycle) > } > > ee.events = EPOLLET|EPOLLIN|EPOLLRDHUP; > + ee.data.u64 = 0; > > if (epoll_ctl(ep, EPOLL_CTL_ADD, s[0], &ee) == -1) { > ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, There were reports about similar complaints from Valgrind on 32-bit platforms a while ago, but for normal epoll_ctl() calls, where only data.ptr is set: https://mailman.nginx.org/pipermail/nginx-devel/2013-July/003892.html Given this is a hot path, and there are multiple such calls, suggested approach was to do additional unneeded initialization only if NGX_VALGRIND is defined (which is already used to ignore sigaction() results under Valgrind, see 5244:593d344999f5). This was never implemented though. Still, I cannot reproduce it with recent Valgrind versions. It looks like it is fixed in the Valgrind itself, and now it only checks ee.events, but not ee.data and padding: https://bugs.kde.org/show_bug.cgi?id=422623 The fix is available starting with Valgrind 4.17.0, which is not exactly new. In particular, the only supported Ubuntu version without the fix is 20.04 LTS, while Ubuntu 22.04 LTS packages contain Valgrind 3.18.0, with the fix. OTOH, I'm not really seeing this on Ubuntu 20.04 LTS / amd64 with Valgrind 3.15.0 either, not sure why, probably due to some code generation nuances. But on Ubuntu 20.04 LTS I'm seeing another Valgrind bug ("Source and destination overlap in memcpy_chk()" for memmove(), https://bugs.kde.org/show_bug.cgi?id=402833). I was only able to reproduce this on Ubuntu 18.04 / i386 (with Valgrind 3.13.0, along with complaints about normal epoll_ctl() calls. Overall, this patch is probably no longer needed, as the proper fix is already in Valgrind, and there are other bugs in old Valgrind versions anyway. -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Wed May 15 07:37:23 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Wed, 15 May 2024 10:37:23 +0300 Subject: [nginx] Version bump. Message-ID: details: http://freenginx.org/hg/nginx/rev/c15c26c99727 branches: changeset: 9273:c15c26c99727 user: Maxim Dounin date: Wed May 15 10:03:03 2024 +0300 description: Version bump. diffstat: src/core/nginx.h | 4 ++-- 1 files changed, 2 insertions(+), 2 deletions(-) diffs (14 lines): diff --git a/src/core/nginx.h b/src/core/nginx.h --- a/src/core/nginx.h +++ b/src/core/nginx.h @@ -9,8 +9,8 @@ #define _NGINX_H_INCLUDED_ -#define nginx_version 1027000 -#define NGINX_VERSION "1.27.0" +#define nginx_version 1027001 +#define NGINX_VERSION "1.27.1" #define NGINX_NAME "freenginx" #define NGINX_VER NGINX_NAME "/" NGINX_VERSION From mdounin at mdounin.ru Wed May 15 07:37:23 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Wed, 15 May 2024 10:37:23 +0300 Subject: [nginx] Mail: reset imap tag to empty after authentication attempt. Message-ID: details: http://freenginx.org/hg/nginx/rev/46ecad404a29 branches: changeset: 9274:46ecad404a29 user: Rob Mueller date: Wed May 15 10:06:00 2024 +0300 description: Mail: reset imap tag to empty after authentication attempt. We need to reset the imap tag to empty after an authentication attempt completes, otherwise if the next line parsed is incomplete with no tag (e.g. empty line) then we use the "tag" from the previous buffer which is now definitely wrong and has been partially overwritten with the most recently read data (e.g. CRLF). An example before this patch: S: * OK IMAP4 ready C: foobar login a b S: foobar NO Incorrect username or password. C: S: S: obar BAD invalid command Then with this patch: S: * OK IMAP4 ready C: foobar login a b S: foobar NO Incorrect username or password. C: S: * BAD invalid command diffstat: src/mail/ngx_mail_auth_http_module.c | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diffs (11 lines): diff --git a/src/mail/ngx_mail_auth_http_module.c b/src/mail/ngx_mail_auth_http_module.c --- a/src/mail/ngx_mail_auth_http_module.c +++ b/src/mail/ngx_mail_auth_http_module.c @@ -883,6 +883,7 @@ ngx_mail_auth_sleep_handler(ngx_event_t s->mail_state = 0; s->auth_method = NGX_MAIL_AUTH_PLAIN; + s->tag.len = 0; c->log->action = "in auth state"; From mdounin at mdounin.ru Wed May 15 07:37:59 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Wed, 15 May 2024 10:37:59 +0300 Subject: [nginx] Reset imap tag to empty after authentication attempt In-Reply-To: <3248a5e6-f931-4cca-a219-44d2451b837c@app.fastmail.com> References: <3248a5e6-f931-4cca-a219-44d2451b837c@app.fastmail.com> Message-ID: Hello! On Mon, May 13, 2024 at 03:43:18PM +1000, Robert Mueller wrote: > # HG changeset patch > # User Rob Mueller > Reset imap tag to empty after authentication attempt > > We need to reset the imap tag to empty after an authentication attempt > completes, otherwise if the next line parsed is incomplete with no tag > (e.g. empty line) then we use the "tag" from the previous buffer which > is now definitely wrong and has been partially overwritten with the most > recently read data (e.g. CRLF). > > An example before this patch: > > S: * OK IMAP4 ready > C: foobar login a b > S: foobar NO Incorrect username or password. > C: > S: > S: obar BAD invalid command > > Then with this patch: > > S: * OK IMAP4 ready > C: foobar login a b > S: foobar NO Incorrect username or password. > C: > S: * BAD invalid command > --- > src/mail/ngx_mail_auth_http_module.c | 1 + > 1 file changed, 1 insertion(+) > > diff --git a/src/mail/ngx_mail_auth_http_module.c b/src/mail/ngx_mail_auth_http_module.c > index 543d1c4eb..55280d66c 100644 > --- a/src/mail/ngx_mail_auth_http_module.c > +++ b/src/mail/ngx_mail_auth_http_module.c > @@ -981,6 +981,7 @@ ngx_mail_auth_sleep_handler(ngx_event_t *rev) > > s->mail_state = 0; > s->auth_method = NGX_MAIL_AUTH_PLAIN; > + s->tag.len = 0; > > c->log->action = "in auth state"; > Looks good to me. Committed with minimal fixes to commit log, thanks. -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Wed May 15 23:36:01 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Thu, 16 May 2024 02:36:01 +0300 Subject: [nginx-tests] Tests: added test for WAIT mail auth response. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/913d96252b7a branches: changeset: 1977:913d96252b7a user: Rob Mueller date: Wed May 15 21:54:16 2024 +0300 description: Tests: added test for WAIT mail auth response. diffstat: mail_auth_wait.t | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 161 insertions(+), 0 deletions(-) diffs (166 lines): diff --git a/mail_auth_wait.t b/mail_auth_wait.t new file mode 100644 --- /dev/null +++ b/mail_auth_wait.t @@ -0,0 +1,161 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for nginx mail module for WAIT auth response. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::IMAP; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +local $SIG{PIPE} = 'IGNORE'; + +my $t = Test::Nginx->new()->has(qw/mail imap http map rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +mail { + proxy_pass_error_message on; + proxy_timeout 15s; + auth_http http://127.0.0.1:8080/mail/auth; + + server { + listen 127.0.0.1:8143; + protocol imap; + imap_auth plain cram-md5 external; + } +} + +http { + %%TEST_GLOBALS_HTTP%% + + map $upstream_http_count $reply { + # Each wait is 1 second, so wait 4 times, which should get us + # to after the sleep(3), but then after another sleep(2) we + # should have completed with a success + + 1 WAIT; + 2 WAIT; + 3 WAIT; + 4 WAIT; + 5 OK; + + default ERROR; + } + + log_format test "reply=$reply"; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + access_log %%TESTDIR%%/auth.log test; + + location = /mail/auth { + add_header Auth-Status $reply; + add_header Auth-Server 127.0.0.1; + add_header Auth-Port %%PORT_8144%%; + add_header Auth-Pass ""; + add_header Auth-Wait 1; + proxy_pass http://127.0.0.1:8081; + } + } +} + +EOF + +$t->run_daemon(\&Test::Nginx::IMAP::imap_test_daemon); +$t->run_daemon(\&http_daemon); +$t->run()->plan(4); + +$t->waitforsocket('127.0.0.1:' . port(8144)); +$t->waitforsocket('127.0.0.1:' . port(8081)); + +############################################################################### + +# WAIT response + +my $s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('a01 LOGIN test at example.com wait'); + +sleep(3); + +my $f = $t->read_file('auth.log'); +my @waits = $f =~ /^reply=WAIT/mg; +ok(@waits >= 2, "found multiple WAIT responses in log"); + +my @ready = $s->can_read(0.1); +is(scalar @ready, 0, "nothing to read while waiting"); + +sleep(2); + + at ready = $s->can_read(0); +is(scalar @ready, 1, "ready for reading"); + +$s->ok('login success after waiting'); + +############################################################################### + +sub http_daemon { + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1', + LocalPort => port(8081), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + my $count = 0; + + while (my $client = $server->accept()) { + $client->autoflush(1); + + my $headers = ''; + my $uri = ''; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + + next if $headers eq ''; + $count++; + + Test::Nginx::log_core('||', "response, $count"); + print $client < details: http://freenginx.org/hg/nginx-tests/rev/79753dd514e6 branches: changeset: 1978:79753dd514e6 user: Rob Mueller date: Wed May 15 23:51:31 2024 +0300 description: Tests: added test for mail proxy timeout. diffstat: mail_proxy_timeout.t | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 121 insertions(+), 0 deletions(-) diffs (126 lines): diff --git a/mail_proxy_timeout.t b/mail_proxy_timeout.t new file mode 100644 --- /dev/null +++ b/mail_proxy_timeout.t @@ -0,0 +1,121 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for nginx mail module, timeout and proxy_timeout directives. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::IMAP; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +local $SIG{PIPE} = 'IGNORE'; + +my $t = Test::Nginx->new()->has(qw/mail imap http map rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +mail { + proxy_pass_error_message on; + timeout 2s; + proxy_timeout 2s; + auth_http http://127.0.0.1:8080/mail/auth; + + server { + listen 127.0.0.1:8143; + protocol imap; + imap_auth plain cram-md5 external; + } +} + +http { + %%TEST_GLOBALS_HTTP%% + + map $http_auth_pass $reply { + secret OK; + default ERROR; + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location = /mail/auth { + add_header Auth-Status $reply; + add_header Auth-Server 127.0.0.1; + add_header Auth-Port %%PORT_8144%%; + add_header Auth-Pass ""; + add_header Auth-Wait 1; + return 204; + } + } +} + +EOF + +$t->run_daemon(\&Test::Nginx::IMAP::imap_test_daemon); +$t->run()->plan(8); + +$t->waitforsocket('127.0.0.1:' . port(8144)); + +############################################################################### + +# check proxy timeout + +my $s = Test::Nginx::IMAP->new(); +$s->read(); + +# Each of these will wait 1 second before response + +$s->send('a01 LOGIN test at example.com bad'); +$s->check(qr/^a01 NO/, 'login with bad password'); + +$s->send('a01 LOGIN test at example.com bad'); +$s->check(qr/^a01 NO/, 'login with bad password'); + +sleep(1); + +# Total timeout is 2 seconds, so connection should have been closed + +my @ready = $s->can_read(0); +is(scalar @ready, 1, "ready for reading"); +ok($s->eof(), "session closed"); + + +$s = Test::Nginx::IMAP->new(); +$s->read(); + +$s->send('a01 LOGIN test at example.com secret'); +$s->ok('login'); + + at ready = $s->can_read(0.1); +is(scalar @ready, 0, "nothing to read after login"); + +sleep(3); + +# Total timeout is 2 seconds, so connection should have been closed + + at ready = $s->can_read(0); +is(scalar @ready, 1, "ready for reading"); +ok($s->eof(), "session closed"); + +############################################################################### From mdounin at mdounin.ru Wed May 15 23:37:14 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Thu, 16 May 2024 02:37:14 +0300 Subject: [nginx-tests] Add test for WAIT auth response In-Reply-To: References: Message-ID: Hello! On Mon, May 13, 2024 at 04:38:56PM +1000, Robert Mueller wrote: > # HG changeset patch > # User Rob Mueller > Add test for WAIT auth response > > --- > mail_auth_wait.t | 164 +++++++++++++++++++++++++++++++++++++++++++++++ > 1 file changed, 164 insertions(+) > create mode 100644 mail_auth_wait.t > > diff --git a/mail_auth_wait.t b/mail_auth_wait.t > new file mode 100644 > index 0000000..6132aaa > --- /dev/null > +++ b/mail_auth_wait.t > @@ -0,0 +1,164 @@ > +#!/usr/bin/perl > + > +# (C) Maxim Dounin > + > +# Tests for nginx mail module for WAIT response. > + > +############################################################################### > + > +use warnings; > +use strict; > + > +use Test::More; > + > +use MIME::Base64; > +use Socket qw/ CRLF /; > + > +BEGIN { use FindBin; chdir($FindBin::Bin); } > + > +use lib 'lib'; > +use Test::Nginx; > +use Test::Nginx::IMAP; > + > +############################################################################### > + > +select STDERR; $| = 1; > +select STDOUT; $| = 1; > + > +local $SIG{PIPE} = 'IGNORE'; > + > +my $t = Test::Nginx->new()->has(qw/mail imap http rewrite/) > + ->write_file_expand('nginx.conf', <<'EOF'); > + > +%%TEST_GLOBALS%% > + > +daemon off; > + > +events { > +} > + > +mail { > + proxy_pass_error_message on; > + proxy_timeout 15s; > + auth_http http://127.0.0.1:8080/mail/auth; > + > + server { > + listen 127.0.0.1:8143; > + protocol imap; > + imap_auth plain cram-md5 external; > + } > +} > + > +http { > + %%TEST_GLOBALS_HTTP%% > + > + map $upstream_http_count $reply { > + # There won't be a 1, the waitforsocket(...) will get that > + > + # Each wait is 1 second, so wait 4 times, which should get us > + # to after the sleep(3), but then after another sleep(2) we > + # should have completed with a success > + 2 WAIT; > + 3 WAIT; > + 4 WAIT; > + 5 WAIT; > + 6 OK; > + default ERROR; > + } > + > + log_format test "reply=$reply"; > + > + server { > + listen 127.0.0.1:8080; > + server_name localhost; > + > + access_log %%TESTDIR%%/auth.log test; > + > + location = /mail/auth { > + add_header Auth-Status $reply; > + add_header Auth-Server 127.0.0.1; > + add_header Auth-Port %%PORT_8144%%; > + add_header Auth-Pass ""; > + add_header Auth-Wait 1; > + proxy_pass http://127.0.0.1:8083; > + } > + } > +} > + > +EOF > + > +$t->run_daemon(\&Test::Nginx::IMAP::imap_test_daemon); > +$t->run_daemon(\&http_daemon, port(8083)); > +$t->run()->plan(4); > + > +$t->waitforsocket('127.0.0.1:' . port(8144)); > +$t->waitforsocket('127.0.0.1:' . port(8083)); > + > +############################################################################### > + > +# WAIT response > + > +my $s = Test::Nginx::IMAP->new(); > +$s->read(); > +$s->send('a01 LOGIN test at example.com wait'); > +note("sent LOGIN, sleeping"); > + > +sleep(3); > + > +my $f = $t->read_file('auth.log'); > +my @waits = $f =~ /^reply=WAIT/mg; > +ok(@waits >= 2, "found multiple WAIT responses in log"); > + > +my @ready = $s->can_read(0.1); > +is(scalar @ready, 0, "nothing to read while waiting"); > + > +sleep(2); > + > + at ready = $s->can_read(0); > +is(scalar @ready, 1, "ready for reading"); > + > +$s->ok('login success after waiting'); > + > +############################################################################### > + > +sub http_daemon { > + my ($port) = @_; > + > + my $server = IO::Socket::INET->new( > + Proto => 'tcp', > + LocalHost => '127.0.0.1', > + LocalPort => $port, > + Listen => 5, > + Reuse => 1 > + ) > + or die "Can't create listening socket: $!\n"; > + > + local $SIG{PIPE} = 'IGNORE'; > + > + my $count = 0; > + while (my $client = $server->accept()) { > + $count++; > + $client->autoflush(1); > + > + my $headers = ''; > + my $uri = ''; > + > + while (<$client>) { > + $headers .= $_; > + last if (/^\x0d?\x0a?$/); > + } > + > + next if $headers eq ''; > + > + Test::Nginx::log_core('||', "$port: response, 204"); > + print $client < +HTTP/1.1 204 No content > +Count: $count > +Connection: close > + > +EOF > + > + } continue { > + close $client; > + } > +} Committed (with minor cleanup), thanks. -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Wed May 15 23:38:08 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Thu, 16 May 2024 02:38:08 +0300 Subject: [nginx-tests] Add test for mail proxy timeout In-Reply-To: References: Message-ID: Hello! On Mon, May 13, 2024 at 04:41:43PM +1000, Robert Mueller wrote: > # HG changeset patch > # User Rob Mueller > Add test for mail proxy timeout > > --- > mail_proxy_timeout.t | 124 +++++++++++++++++++++++++++++++++++++++++++ > 1 file changed, 124 insertions(+) > create mode 100644 mail_proxy_timeout.t > > diff --git a/mail_proxy_timeout.t b/mail_proxy_timeout.t > new file mode 100644 > index 0000000..f092d4b > --- /dev/null > +++ b/mail_proxy_timeout.t > @@ -0,0 +1,124 @@ > +#!/usr/bin/perl > + > +# (C) Maxim Dounin > + > +# Tests for nginx mail module for WAIT response. > + > +############################################################################### > + > +use warnings; > +use strict; > + > +use Test::More; > + > +use MIME::Base64; > +use Socket qw/ CRLF /; > + > +BEGIN { use FindBin; chdir($FindBin::Bin); } > + > +use lib 'lib'; > +use Test::Nginx; > +use Test::Nginx::IMAP; > + > +############################################################################### > + > +select STDERR; $| = 1; > +select STDOUT; $| = 1; > + > +local $SIG{PIPE} = 'IGNORE'; > + > +my $t = Test::Nginx->new()->has(qw/mail imap http rewrite/) > + ->write_file_expand('nginx.conf', <<'EOF'); > + > +%%TEST_GLOBALS%% > + > +daemon off; > + > +events { > +} > + > +mail { > + proxy_pass_error_message on; > + timeout 2s; > + proxy_timeout 2s; > + auth_http http://127.0.0.1:8080/mail/auth; > + > + server { > + listen 127.0.0.1:8143; > + protocol imap; > + imap_auth plain cram-md5 external; > + } > +} > + > +http { > + %%TEST_GLOBALS_HTTP%% > + > + map $http_auth_pass $reply { > + secret OK; > + default ERROR; > + } > + > + server { > + listen 127.0.0.1:8080; > + server_name localhost; > + > + location = /mail/auth { > + add_header Auth-Status $reply; > + add_header Auth-Server 127.0.0.1; > + add_header Auth-Port %%PORT_8144%%; > + add_header Auth-Pass ""; > + add_header Auth-Wait 1; > + return 204; > + } > + } > +} > + > +EOF > + > +$t->run_daemon(\&Test::Nginx::IMAP::imap_test_daemon); > +$t->run()->plan(8); > + > +$t->waitforsocket('127.0.0.1:' . port(8144)); > + > +############################################################################### > + > +# check proxy timeout > + > +my $s = Test::Nginx::IMAP->new(); > +$s->read(); > + > +# Each of these will wait 1 second before response > + > +$s->send('a01 LOGIN test at example.com bad'); > +$s->check(qr/^a01 NO/, 'login with bad password'); > + > +$s->send('a01 LOGIN test at example.com bad'); > +$s->check(qr/^a01 NO/, 'login with bad password'); > + > +sleep(1); > + > +# Total timeout is 2 seconds, so connection should have been closed > + > +my @ready = $s->can_read(0); > +is(scalar @ready, 1, "ready for reading"); > +ok($s->eof(), "session closed"); > + > + > +$s = Test::Nginx::IMAP->new(); > +$s->read(); > + > +$s->send('a01 LOGIN test at example.com secret'); > +$s->ok('login'); > + > + at ready = $s->can_read(0.1); > +is(scalar @ready, 0, "nothing to read after login"); > + > +sleep(3); > + > +# Total timeout is 2 seconds, so connection should have been closed > + > + at ready = $s->can_read(0); > +is(scalar @ready, 1, "ready for reading"); > +ok($s->eof(), "session closed"); > + > +############################################################################### Committed (with minor cleanup), thanks. -- Maxim Dounin http://mdounin.ru/ From maksim.yevmenkin at gmail.com Thu May 16 00:27:51 2024 From: maksim.yevmenkin at gmail.com (Maksim Yevmenkin) Date: Wed, 15 May 2024 17:27:51 -0700 Subject: limiting number of HTTP request headers Message-ID: Hello! Could the community share their thoughts on introducing a directive to cap the number of HTTP request headers? While we currently have the ability to limit client HTTP request buffer size, having more specific control over the number of headers could be advantageous. Thanks, max From mdounin at mdounin.ru Thu May 16 07:45:49 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Thu, 16 May 2024 10:45:49 +0300 Subject: [nginx-tests] Add valgrind support to nginx test suite In-Reply-To: References: Message-ID: Hello! On Mon, May 13, 2024 at 04:43:00PM +1000, Robert Mueller wrote: > # HG changeset patch > # User Rob Mueller > Add valgrind support to nginx test suite > > Setting TEST_NGINX_VALGRIND=1 will cause the test suite > to run nginx under valgrind and check at exit that there's > no errors present > --- > lib/Test/Nginx.pm | 13 +++++++++++-- > 1 file changed, 11 insertions(+), 2 deletions(-) > > diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm > index 7b7bdc5..f363349 100644 > --- a/lib/Test/Nginx.pm > +++ b/lib/Test/Nginx.pm > @@ -86,7 +86,12 @@ sub DESTROY { > local $Test::Nginx::TODO; > my $errors = $self->read_file('error.log'); > $errors = join "\n", $errors =~ /.+Sanitizer.+/gm; > - Test::More::is($errors, '', 'no sanitizer errors'); > + my $extra = ""; > + if ($ENV{TEST_NGINX_VALGRIND}) { > + $extra = "/valgrind"; > + $errors .= $self->read_file('valgrind.log'); > + } > + Test::More::is($errors, '', "no sanitizer${extra} errors"); > } > > if ($ENV{TEST_NGINX_CATLOG}) { > @@ -398,7 +403,11 @@ sub run(;$) { > my @globals = $self->{_test_globals} ? > () : ('-g', "pid $testdir/nginx.pid; " > . "error_log $testdir/error.log debug;"); > - exec($NGINX, '-p', "$testdir/", '-c', 'nginx.conf', > + my @cmd = ($NGINX); > + if ($ENV{TEST_NGINX_VALGRIND}) { > + unshift @cmd, 'valgrind', '-q', "--log-file=$testdir/valgrind.log"; > + } > + exec(@cmd, '-p', "$testdir/", '-c', 'nginx.conf', > '-e', 'error.log', @globals) > or die "Unable to exec(): $!\n"; > } In my experience, running tests under Valgrind is usually a challenge, and it rarely make sense now, as compiler-provided sanitizers are mostly equivalent (and way faster). Further, it probably can be done via custom TEST_NGINX_BINARY with a wrapper which runs nginx under Valgrind. Still, I personally used to patch run() when needed, and introducing an explicit support for Valgrind might be beneficial at least for some. Below are patches which clean up related areas, and introduce an explicit Valgrind support as a separate destructor test. Note that Valgrind logging seems to interfere with error suppression in tests and catches various startup warnings, so errors are filtered similarly to how it is done with error.log to catch sanitizer errors. I also used an exec() code which matches existing approach with adding @globals, and modified waitforfile() timeout so that tests don't fail due to very slow startup times with Valgrind. Also there is a couple of fixes of issues as reported by Valgrind during full test run, see below. With these patches, I am able to run all tests with Valgrind without any real test failures. (Still, some tests occasionally fail due to Valgrind being very slow.) Please let me know if it looks good for you. # HG changeset patch # User Maxim Dounin # Date 1715843768 -10800 # Thu May 16 10:16:08 2024 +0300 # Node ID 32b470da408173624ad598d4bdc919dac0bbe59f # Parent 79753dd514e60b47375e36c54739ea434e04a5b6 Tests: avoid changing non-localized $TODO. This ensures that there will be no unrelated effects if the variable is actually changed, such as seen on sanitizer tests in 910:49579dd88e3f (reverted by this change). diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -69,21 +69,19 @@ sub DESTROY { my @alerts = $self->read_file('error.log') =~ /.+\[alert\].+/gm; - if ($^O eq 'solaris') { - $Test::Nginx::TODO = 'alerts' if @alerts - && ! grep { $_ !~ /phantom event/ } @alerts; - } - if ($^O eq 'MSWin32') { - my $re = qr/CloseHandle|TerminateProcess/; - $Test::Nginx::TODO = 'alerts' if @alerts - && ! grep { $_ !~ $re } @alerts; - } + local $Test::Nginx::TODO = 'alerts' if @alerts + && $^O eq 'solaris' + && ! grep { $_ !~ /phantom event/ } @alerts; + + local $Test::Nginx::TODO = 'alerts' if @alerts + && $^O eq 'MSWin32' + && ! grep { $_ !~ qr/CloseHandle|TerminateProcess/ } + @alerts; Test::More::is(join("\n", @alerts), '', 'no alerts'); } if (Test::More->builder->expected_tests) { - local $Test::Nginx::TODO; my $errors = $self->read_file('error.log'); $errors = join "\n", $errors =~ /.+Sanitizer.+/gm; Test::More::is($errors, '', 'no sanitizer errors'); # HG changeset patch # User Maxim Dounin # Date 1715843977 -10800 # Thu May 16 10:19:37 2024 +0300 # Node ID 28670f23380ac08a0121d0a97cdb6a092bb01a31 # Parent 32b470da408173624ad598d4bdc919dac0bbe59f Tests: explicit Valgrind support. Valgrind logging is done to a separate file, as it is not able to follow stderr redirection within nginx or append to a file without corrupting it. Further, Valgrind logging seems to interfere with error suppression in tests, and catches various startup errors and warnings, so the log is additionally filtered. Since startup under Valgrind can be really slow, timeout in waitforfile() was changed to 10 seconds. Prodded by Robert Mueller. diff --git a/README b/README --- a/README +++ b/README @@ -40,6 +40,10 @@ TEST_NGINX_UNSAFE Run unsafe tests. +TEST_NGINX_VALGRIND + + Run nginx under Valgrind during tests. + TEST_NGINX_GLOBALS Sets additional directives in main context. diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -87,6 +87,12 @@ sub DESTROY { Test::More::is($errors, '', 'no sanitizer errors'); } + if (Test::More->builder->expected_tests && $ENV{TEST_NGINX_VALGRIND}) { + my $errors = $self->read_file('valgrind.log'); + $errors = join "\n", $errors =~ /^==\d+== .+/gm; + Test::More::is($errors, '', 'no valgrind errors'); + } + if ($ENV{TEST_NGINX_CATLOG}) { system("cat $self->{_testdir}/error.log"); } @@ -365,7 +371,10 @@ sub try_run($$) { sub plan($) { my ($self, $plan) = @_; - Test::More::plan(tests => $plan + 2); + $plan += 2; + $plan += 1 if $ENV{TEST_NGINX_VALGRIND}; + + Test::More::plan(tests => $plan); return $self; } @@ -395,7 +404,10 @@ sub run(;$) { my @globals = $self->{_test_globals} ? () : ('-g', "pid $testdir/nginx.pid; " . "error_log $testdir/error.log debug;"); - exec($NGINX, '-p', "$testdir/", '-c', 'nginx.conf', + my @valgrind = (not $ENV{TEST_NGINX_VALGRIND}) ? + () : ('valgrind', '-q', + "--log-file=$testdir/valgrind.log"); + exec(@valgrind, $NGINX, '-p', "$testdir/", '-c', 'nginx.conf', '-e', 'error.log', @globals) or die "Unable to exec(): $!\n"; } @@ -481,7 +493,7 @@ sub waitforfile($;$) { # wait for file to appear # or specified process to exit - for (1 .. 50) { + for (1 .. 100) { return 1 if -e $file; return 0 if $exited; $exited = waitpid($pid, WNOHANG) != 0 if $pid; And here is a patch which fixes issues as reported by Valgrind: # HG changeset patch # User Maxim Dounin # Date 1715840681 -10800 # Thu May 16 09:24:41 2024 +0300 # Node ID 7fe30d2ccac58c91b057291ab1eee7949cbd6f69 # Parent 46ecad404a296042c0088e699f275a92758e5ab9 Fixed Valgrind complaints about uninitialized values. In ngx_http_source_charset(), name->data was left uninitialized, and only name->len was set. Since it is used in debug logging, this resulted in the following complaints from Valgrind: ==42== Conditional jump or move depends on uninitialised value(s) ==42== at 0x12BC66: memcpy (string.h:51) ==42== by 0x12BC66: ngx_sprintf_str (ngx_string.c:586) ==42== by 0x12C03C: ngx_vslprintf (ngx_string.c:255) ==42== by 0x127694: ngx_log_error_core (ngx_log.c:135) ==42== by 0x1B8795: ngx_http_charset_header_filter (ngx_http_charset_filter_module.c:252) Similarly, ngx_http_split_args() returned uninitialized arg->data, which was then copied to r->args, and also used in debug logging: ==42== Conditional jump or move depends on uninitialised value(s) ==42== at 0x12BC10: memcpy (string.h:50) ==42== by 0x12BC10: ngx_sprintf_str (ngx_string.c:586) ==42== by 0x12C03C: ngx_vslprintf (ngx_string.c:255) ==42== by 0x127694: ngx_log_error_core (ngx_log.c:135) ==42== by 0x184EFB: ngx_http_internal_redirect (ngx_http_core_module.c:2526) ==42== by 0x1D8CCC: ngx_http_try_files_handler (ngx_http_try_files_module.c:209) Fix is to initialize data to NULL. Note that, while memcpy(p, NULL, 0) is also formally undefined now, it is used in multiple places in the code, and expected to be allowed in C2y (see WG14 proposals N3177, N3261, "Allow zero length operations on null pointers"). Prodded by Valgrind. diff --git a/src/http/modules/ngx_http_charset_filter_module.c b/src/http/modules/ngx_http_charset_filter_module.c --- a/src/http/modules/ngx_http_charset_filter_module.c +++ b/src/http/modules/ngx_http_charset_filter_module.c @@ -438,6 +438,7 @@ ngx_http_source_charset(ngx_http_request if (charset == NGX_HTTP_CHARSET_OFF) { name->len = 0; + name->data = NULL; return charset; } diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -2146,6 +2146,7 @@ ngx_http_split_args(ngx_http_request_t * } else { args->len = 0; + args->data = NULL; } } -- Maxim Dounin http://mdounin.ru/ From mux99 at live.be Thu May 16 12:38:53 2024 From: mux99 at live.be (=?iso-8859-1?q?maxime?=) Date: Thu, 16 May 2024 14:38:53 +0200 Subject: [PATCH 1 of 4] Core: added socket protocol Message-ID: # HG changeset patch # User maxime # Date 1715588655 -7200 # Mon May 13 10:24:15 2024 +0200 # Node ID dcadf0a3d97ff4d677440060290e9cda84c69ac7 # Parent 3d455e37abf870f79be26c36d6b1d9cad2c4dd16 Core: added socket protocol. This patch updates the creation of listening sockets to use a new field of the `ngx_listening_s` structure. The `protocol` field can be used in conjunction with the `type` to specify the protocol to be used. Modules will then be able to specify a different protocol, e.g. IPPROTO_MPTCP. diff --git a/src/core/ngx_connection.c b/src/core/ngx_connection.c --- a/src/core/ngx_connection.c +++ b/src/core/ngx_connection.c @@ -487,7 +487,18 @@ ngx_open_listening_sockets(ngx_cycle_t * continue; } - s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); + s = (ngx_socket_t) -1; + if (ls[i].protocol > 0) { + s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, + ls[i].protocol); + /* In case of error, retry with the default protocol */ + ngx_log_error(NGX_LOG_NOTICE, log, 0, + "socket(%d) failed, trying with 0", ls[i].protocol); + } + + if (s == (ngx_socket_t) -1) { + s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); + } if (s == (ngx_socket_t) -1) { ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno, diff --git a/src/core/ngx_connection.h b/src/core/ngx_connection.h --- a/src/core/ngx_connection.h +++ b/src/core/ngx_connection.h @@ -24,6 +24,7 @@ struct ngx_listening_s { ngx_str_t addr_text; int type; + int protocol; int backlog; int rcvbuf; From mux99 at live.be Thu May 16 12:38:54 2024 From: mux99 at live.be (=?iso-8859-1?q?maxime?=) Date: Thu, 16 May 2024 14:38:54 +0200 Subject: [PATCH 2 of 4] HTTP: added MPTCP support In-Reply-To: References: Message-ID: # HG changeset patch # User maxime # Date 1715859841 -7200 # Thu May 16 13:44:01 2024 +0200 # Node ID 74012b4ab947ce4b3e42cb75a7239f0447aacffc # Parent dcadf0a3d97ff4d677440060290e9cda84c69ac7 HTTP: added MPTCP support. Multipath TCP (MPTCP), standardized in RFC8684 [1], is a TCP extension that enables a TCP connection to use different paths. Multipath TCP has been used for several use cases. On smartphones, MPTCP enables seamless handovers between cellular and Wi-Fi networks while preserving Established connections. This use-case is what pushed Apple to use MPTCP since 2013 in multiple applications [2]. On dual-stack hosts, Multipath TCP enables the TCP connection to automatically use the best performing path, either IPv4 or IPv6. If one path fails, MPTCP automatically uses the other path. The benefit from MPTCP, both the client and the server have to support it. Multipath TCP is a backward-compatible TCP extension that is enabled by default on recent Linux distributions (Debian, Ubuntu, Redhat, ...). Multipath TCP is included in the Linux kernel since version 5.6 [3]. To use it on Linux, an application must explicitly enable it when creating the socket. No need to change anything else in the application. Even if MPTCP is supported by different OS, only Linux supports the `IPPROTO_MPTCP` protocol, which is why this feature is currently limited to Linux only. This patch adds a new parameter 'mptcp' to the 'listen' directive in the HTTP module. Link: [https://www.rfc=editor.org/rfc/rfc8684.html]https://www.rfc=editor.org/rfc/rfc8684.html [1] Link: https://www.tessares.net/apples-mptcp=story-so-far/ [2] Link: https://www.mptcp.dev [3] diff --git a/contrib/vim/syntax/nginx.vim b/contrib/vim/syntax/nginx.vim --- a/contrib/vim/syntax/nginx.vim +++ b/contrib/vim/syntax/nginx.vim @@ -65,7 +65,7 @@ syn match ngxListenComment '#.*$' \ contained \ nextgroup=@ngxListenParams skipwhite skipempty syn keyword ngxListenOptions contained - \ default_server ssl quic proxy_protocol + \ default_server ssl quic proxy_protocol mptcp \ setfib fastopen backlog rcvbuf sndbuf accept_filter deferred bind \ ipv6only reuseport so_keepalive \ nextgroup=@ngxListenParams skipwhite skipempty diff --git a/src/http/ngx_http.c b/src/http/ngx_http.c --- a/src/http/ngx_http.c +++ b/src/http/ngx_http.c @@ -1845,6 +1845,7 @@ ngx_http_add_listening(ngx_conf_t *cf, n #endif ls->type = addr->opt.type; + ls->protocol = addr->opt.protocol; ls->backlog = addr->opt.backlog; ls->rcvbuf = addr->opt.rcvbuf; ls->sndbuf = addr->opt.sndbuf; diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c --- a/src/http/ngx_http_core_module.c +++ b/src/http/ngx_http_core_module.c @@ -20,6 +20,10 @@ typedef struct { #define NGX_HTTP_REQUEST_BODY_FILE_ON 1 #define NGX_HTTP_REQUEST_BODY_FILE_CLEAN 2 +#ifndef IPPROTO_MPTCP +#define IPPROTO_MPTCP 262 +#endif + static ngx_int_t ngx_http_core_auth_delay(ngx_http_request_t *r); static void ngx_http_core_auth_delay_handler(ngx_http_request_t *r); @@ -4052,6 +4056,13 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx } #endif +#if (NGX_LINUX) + if (ngx_strcmp(value[n].data, "mptcp") == 0) { + lsopt.protocol = IPPROTO_MPTCP; + continue; + } +#endif + if (ngx_strncmp(value[n].data, "backlog=", 8) == 0) { lsopt.backlog = ngx_atoi(value[n].data + 8, value[n].len - 8); lsopt.set = 1; @@ -4341,6 +4352,12 @@ ngx_http_core_listen(ngx_conf_t *cf, ngx } #endif +#if (NGX_LINUX) + if (lsopt.protocol == IPPROTO_MPTCP) { + return "\"mptcp\" parameter is incompatible with \"quic\""; + } +#endif + #if (NGX_HTTP_V2) if (lsopt.http2) { return "\"http2\" parameter is incompatible with \"quic\""; diff --git a/src/http/ngx_http_core_module.h b/src/http/ngx_http_core_module.h --- a/src/http/ngx_http_core_module.h +++ b/src/http/ngx_http_core_module.h @@ -88,6 +88,7 @@ typedef struct { int rcvbuf; int sndbuf; int type; + int protocol; #if (NGX_HAVE_SETFIB) int setfib; #endif From mux99 at live.be Thu May 16 12:38:55 2024 From: mux99 at live.be (=?iso-8859-1?q?maxime?=) Date: Thu, 16 May 2024 14:38:55 +0200 Subject: [PATCH 3 of 4] Mail: added MPTCP support In-Reply-To: References: Message-ID: # HG changeset patch # User maxime # Date 1715859913 -7200 # Thu May 16 13:45:13 2024 +0200 # Node ID 71a8642308e94bf1005a8f4927506c142742260b # Parent 74012b4ab947ce4b3e42cb75a7239f0447aacffc Mail: added MPTCP support. Multipath TCP (MPTCP), standardized in RFC8684 [1], is a TCP extension that enables a TCP connection to use different paths. Multipath TCP has been used for several use cases. On smartphones, MPTCP enables seamless handovers between cellular and Wi-Fi networks while preserving established connections. This use-case is what pushed Apple to use MPTCP since 2013 in multiple applications [2]. On dual-stack hosts, Multipath TCP enables the TCP connection to automatically use the best performing path, either IPv4 or IPv6. If one path fails, MPTCP automatically uses the other path. To benefit from MPTCP, both the client and the server have to support it. Multipath TCP is a backward-compatible TCP extension that is enabled by default on recent Linux distributions (Debian, Ubuntu, Redhat, ...). Multipath TCP is included in the Linux kernel since version 5.6 [3]. To use it on Linux, an application must explicitly enable it when creating the socket. No need to change anything else in the application. Even if MPTCP is supported by different OS, only Linux supports the `IPPROTO_MPTCP` protocol, which is why this feature is currently limited to Linux only. This patch adds a new parameter 'mptcp' to the 'listen' directive in the Mail module. Link: https://www.rfc-editor.org/rfc/rfc8684.html [1] Link: https://www.tessares.net/apples-mptcp-story-so-far/ [2] Link: https://www.mptcp.dev [3] diff --git a/src/mail/ngx_mail.c b/src/mail/ngx_mail.c --- a/src/mail/ngx_mail.c +++ b/src/mail/ngx_mail.c @@ -332,6 +332,7 @@ ngx_mail_optimize_servers(ngx_conf_t *cf ls->log.data = &ls->addr_text; ls->log.handler = ngx_accept_log_error; + ls->protocol = addr[i].opt.protocol; ls->backlog = addr[i].opt.backlog; ls->rcvbuf = addr[i].opt.rcvbuf; ls->sndbuf = addr[i].opt.sndbuf; diff --git a/src/mail/ngx_mail.h b/src/mail/ngx_mail.h --- a/src/mail/ngx_mail.h +++ b/src/mail/ngx_mail.h @@ -47,6 +47,7 @@ typedef struct { int tcp_keepintvl; int tcp_keepcnt; #endif + int protocol; int backlog; int rcvbuf; int sndbuf; diff --git a/src/mail/ngx_mail_core_module.c b/src/mail/ngx_mail_core_module.c --- a/src/mail/ngx_mail_core_module.c +++ b/src/mail/ngx_mail_core_module.c @@ -10,6 +10,10 @@ #include #include +#ifndef IPPROTO_MPTCP +#define IPPROTO_MPTCP 262 +#endif + static void *ngx_mail_core_create_main_conf(ngx_conf_t *cf); static void *ngx_mail_core_create_srv_conf(ngx_conf_t *cf); @@ -477,6 +481,12 @@ ngx_mail_core_listen(ngx_conf_t *cf, ngx #endif } + if (ngx_strcmp(value[i].data, "mptcp") == 0) { + ls->protocol = IPPROTO_MPTCP; + continue; + } + + if (ngx_strncmp(value[i].data, "so_keepalive=", 13) == 0) { if (ngx_strcmp(&value[i].data[13], "on") == 0) { From mux99 at live.be Thu May 16 12:38:56 2024 From: mux99 at live.be (=?iso-8859-1?q?maxime?=) Date: Thu, 16 May 2024 14:38:56 +0200 Subject: [PATCH 4 of 4] Stream: added MPTCP support In-Reply-To: References: Message-ID: # HG changeset patch # User maxime # Date 1715859942 -7200 # Thu May 16 13:45:42 2024 +0200 # Node ID bda663d8a1258a10f0312a8d2b272ff98e24fd1f # Parent 71a8642308e94bf1005a8f4927506c142742260b Stream: added MPTCP support. Multipath TCP (MPTCP), standardized in RFC8684 [1], is a TCP extension that enables a TCP connection to use different paths. Multipath TCP has been used for several use cases. On smartphones, MPTCP enables seamless handovers between cellular and Wi-Fi networks while preserving established connections. This use-case is what pushed Apple to use MPTCP since 2013 in multiple applications [2]. On dual-stack hosts, Multipath TCP enables the TCP connection to automatically use the best performing path, either IPv4 or IPv6. If one path fails, MPTCP automatically uses the other path. To benefit from MPTCP, both the client and the server have to support it. Multipath TCP is a backward-compatible TCP extension that is enabled by default on recent Linux distributions (Debian, Ubuntu, Redhat, ...). Multipath TCP is included in the Linux kernel since version 5.6 [3]. To use it on Linux, an application must explicitly enable it when creating the socket. No need to change anything else in the application. Even if MPTCP is supported by different OS, only Linux supports the `IPPROTO_MPTCP` protocol, which is why this feature is currently limited to Linux only. This patch adds a new parameter 'mptcp' to the 'listen' directive in the Stream module. Link: https://www.rfc-editor.org/rfc/rfc8684.html [1] Link: https://www.tessares.net/apples-mptcp-story-so-far/ [2] Link: https://www.mptcp.dev [3] diff --git a/src/stream/ngx_stream.c b/src/stream/ngx_stream.c --- a/src/stream/ngx_stream.c +++ b/src/stream/ngx_stream.c @@ -486,6 +486,7 @@ ngx_stream_optimize_servers(ngx_conf_t * ls->handler = ngx_stream_init_connection; ls->pool_size = 256; ls->type = addr[i].opt.type; + ls->protocol = addr[i].opt.protocol; cscf = addr->opt.ctx->srv_conf[ngx_stream_core_module.ctx_index]; diff --git a/src/stream/ngx_stream.h b/src/stream/ngx_stream.h --- a/src/stream/ngx_stream.h +++ b/src/stream/ngx_stream.h @@ -69,6 +69,7 @@ typedef struct { int fastopen; #endif int type; + int protocol; } ngx_stream_listen_t; diff --git a/src/stream/ngx_stream_core_module.c b/src/stream/ngx_stream_core_module.c --- a/src/stream/ngx_stream_core_module.c +++ b/src/stream/ngx_stream_core_module.c @@ -9,6 +9,11 @@ #include #include +#ifndef IPPROTO_MPTCP +#define IPPROTO_MPTCP 262 +#endif + + static ngx_int_t ngx_stream_core_preconfiguration(ngx_conf_t *cf); static void *ngx_stream_core_create_main_conf(ngx_conf_t *cf); @@ -654,6 +659,14 @@ ngx_stream_core_listen(ngx_conf_t *cf, n } #endif +#if (NGX_LINUX) + if (ngx_strcmp(value[i].data, "mptcp") == 0) { + ls->protocol = IPPROTO_MPTCP; + continue; + } +#endif + + if (ngx_strncmp(value[i].data, "backlog=", 8) == 0) { ls->backlog = ngx_atoi(value[i].data + 8, value[i].len - 8); ls->bind = 1; @@ -884,6 +897,12 @@ ngx_stream_core_listen(ngx_conf_t *cf, n return "\"fastopen\" parameter is incompatible with \"udp\""; } #endif + +#if (NGX_LINUX) + if (ls->protocol == IPPROTO_MPTCP) { + return "\"mptcp\" parameter is incompatible with \"udp\""; + } +#endif } for (n = 0; n < u.naddrs; n++) { From mdounin at mdounin.ru Thu May 16 16:59:32 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Thu, 16 May 2024 19:59:32 +0300 Subject: limiting number of HTTP request headers In-Reply-To: References: Message-ID: Hello! On Wed, May 15, 2024 at 05:27:51PM -0700, Maksim Yevmenkin wrote: > Could the community share their thoughts on introducing a directive to > cap the number of HTTP request headers? While we currently have the > ability to limit client HTTP request buffer size, having more specific > control over the number of headers could be advantageous. I personally tend to think that buffer size limit is enough for [free]nginx itself. On the other hand, an additional limit on the number of request headers might be beneficial to protect backend servers, and might worth adding. -- Maxim Dounin http://mdounin.ru/ From maksim.yevmenkin at gmail.com Thu May 16 18:01:36 2024 From: maksim.yevmenkin at gmail.com (Maksim Yevmenkin) Date: Thu, 16 May 2024 11:01:36 -0700 Subject: limiting number of HTTP request headers In-Reply-To: References: Message-ID: Hello, > > Could the community share their thoughts on introducing a directive to > > cap the number of HTTP request headers? While we currently have the > > ability to limit client HTTP request buffer size, having more specific > > control over the number of headers could be advantageous. > > I personally tend to think that buffer size limit is enough for > [free]nginx itself. On the other hand, an additional limit on the > number of request headers might be beneficial to protect backend > servers, and might worth adding. thanks! a possible patch is attached. max -------------- next part -------------- diff --git a/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.c b/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.c index e26705475e46..b12951fd2a09 100644 --- a/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.c +++ b/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.c @@ -287,6 +287,13 @@ static ngx_command_t ngx_http_core_commands[] = { offsetof(ngx_http_core_srv_conf_t, underscores_in_headers), NULL }, + { ngx_string("max_request_headers"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_HTTP_SRV_CONF_OFFSET, + offsetof(ngx_http_core_srv_conf_t, max_request_headers), + NULL }, + { ngx_string("location"), NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_BLOCK|NGX_CONF_TAKE12, ngx_http_core_location, @@ -3508,6 +3515,7 @@ ngx_http_core_create_srv_conf(ngx_conf_t *cf) cscf->ignore_invalid_headers = NGX_CONF_UNSET; cscf->merge_slashes = NGX_CONF_UNSET; cscf->underscores_in_headers = NGX_CONF_UNSET; + cscf->max_request_headers = NGX_CONF_UNSET_UINT; cscf->file_name = cf->conf_file->file.name.data; cscf->line = cf->conf_file->line; @@ -3554,6 +3562,9 @@ ngx_http_core_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_value(conf->underscores_in_headers, prev->underscores_in_headers, 0); + ngx_conf_merge_value(conf->max_request_headers, + prev->max_request_headers, 128); + if (conf->server_names.nelts == 0) { /* the array has 4 empty preallocated elements, so push cannot fail */ sn = ngx_array_push(&conf->server_names); diff --git a/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.h b/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.h index 3ed9d1632eec..1a5853fc7dc1 100644 --- a/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.h +++ b/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.h @@ -212,6 +212,7 @@ typedef struct { ngx_flag_t ignore_invalid_headers; ngx_flag_t merge_slashes; ngx_flag_t underscores_in_headers; + ngx_uint_t max_request_headers; /* max number of request headers */ unsigned listen:1; #if (NGX_PCRE) diff --git a/ports/netflix/nginx/files/nginx/src/http/ngx_http_request.c b/ports/netflix/nginx/files/nginx/src/http/ngx_http_request.c index 2ed0d99c168b..1ff03b215880 100644 --- a/ports/netflix/nginx/files/nginx/src/http/ngx_http_request.c +++ b/ports/netflix/nginx/files/nginx/src/http/ngx_http_request.c @@ -1728,6 +1728,7 @@ ngx_http_process_request_headers(ngx_event_t *rev) ngx_http_request_t *r; ngx_http_core_srv_conf_t *cscf; ngx_http_core_main_conf_t *cmcf; + ngx_list_part_t *part; c = rev->data; r = c->data; @@ -1816,6 +1817,25 @@ ngx_http_process_request_headers(ngx_event_t *rev) continue; } + /* apply request header limit */ + + for (rv = 0, part = &r->headers_in.headers.part; + part != NULL; + part = part->next) { + rv += part->nelts; + } + + if (rv >= cscf->max_request_headers) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent too many request headers, have %i, limit is %ui", + rv, cscf->max_request_headers); + + r->lingering_close = 1; + ngx_http_finalize_request(r, NGX_HTTP_REQUEST_ENTITY_TOO_LARGE); + + break; + } + /* a header line has been parsed successfully */ h = ngx_list_push(&r->headers_in.headers); From mdounin at mdounin.ru Thu May 16 18:06:28 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Thu, 16 May 2024 21:06:28 +0300 Subject: [PATCH]HTTP/2 connection not properly closing during graceful shutdown In-Reply-To: <0255e7345ffdebf8081563334ed7c931718d2c19.camel@kasei.im> References: <0255e7345ffdebf8081563334ed7c931718d2c19.camel@kasei.im> Message-ID: Hello! On Wed, May 15, 2024 at 11:46:29AM +0800, Kasei Wang wrote: > Hello, > > I found that there is a slight probability of HTTP/2 connections not > properly closing during graceful shutdown, leading to worker processes > in shutting down state remaining stuck for an extended period. After > investigation, the issue appears to stem from the following: > > 1. worker processes in shutting down state use > ngx_close_idle_connections to close all idle connections, including > HTTP/2 connections. > 2. For HTTP/2 connections, c->idle is set to true in ngx_http_v2_init. > According to the explanation in > , GOAWAY should be sent to > all HTTP/2 connections. > 3. There might be a time gap between ngx_event_accept and > ngx_http_v2_init. For TLS connections, ngx_http_v2_init will be > executed after ALPN received, and for plaintext http2 connections, > ngx_http_v2_init will be executed after parsing the http2 preface. If > ngx_close_idle_connections is executed between ngx_event_accept and > ngx_http_v2_init, there's a possibility that c->idle of some > connections is set to true AFTER ngx_close_idle_connections, causing > those connections to not enter the GOAWAY process and leading to the > aforementioned problem. > > To verify this, I've written a simple HTTP/2 client. This program will > wait 15 seconds after TCP connection establishment before starting to > send data. The purpose of sleep is to to raise the probability of > encountering the issue. You can reproduce the problem by executing > "nginx -s reload" during this 15-second wait. If you're interested, you > can try my test program > () to reproduce > the issue. > > The following patch would call ngx_http_v2_finalize_connection to close > http2 connections which is initialized after > ngx_close_idle_connections. > > And here are some previous discussion on the another maillist: > > > > Please confirm if this issue exists, review my analysis and the patch > if possible. Thank you very much. I think your analysis is correct. Thanks for catching this. > # HG changeset patch > # User Kasei Wang > # Date 1715744317 -28800 > # ? ? ?Wed May 15 11:38:37 2024 +0800 > # Node ID 7f18358cc012039f6bb4e502a6f39a7bd50d669b > # Parent ?4cfd64a8330dd5bacd839796732fd8c49d87e86e > HTTP/2: close http2 connections initialized during graceful shutdown. > > In some rare cases, a HTTP/2 connections can be initialized during a > graceful shutdown. Now close such an connection to avoid unexcepted > delays in the graceful shutdown. > > diff -r 4cfd64a8330d -r 7f18358cc012 src/http/v2/ngx_http_v2.c > --- a/src/http/v2/ngx_http_v2.c Tue May 14 17:47:44 2024 +0300 > +++ b/src/http/v2/ngx_http_v2.c Wed May 15 11:38:37 2024 +0800 > @@ -304,6 +304,11 @@ > ? ? ?c->idle = 1; > ? ? ?ngx_reusable_connection(c, 0); > > + ? ?if (ngx_exiting) { > + ? ? ? ?ngx_http_v2_finalize_connection(h2c, NGX_HTTP_V2_NO_ERROR); > + ? ? ? ?return; > + ? ?} > + > ? ? ?if (c->buffer) { > ? ? ? ? ?p = c->buffer->pos; > ? ? ? ? ?end = c->buffer->last; The patch looks working, though in the case in question the code will do various otherwise unneeded things, notably will send SETTINGS and WINDOW_UPDATE frames to the client. OTOH, avoiding these might be tricky and will require more complex changes, and probably don't worth the effort. Another approach might be to allow just one request if a connection is established just before the shutdown, much like HTTP/1.x code does. Something as simple as the following patch should do the trick: diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c --- a/src/http/v2/ngx_http_v2.c +++ b/src/http/v2/ngx_http_v2.c @@ -1336,7 +1336,8 @@ ngx_http_v2_state_headers(ngx_http_v2_co clcf = ngx_http_get_module_loc_conf(h2c->http_connection->conf_ctx, ngx_http_core_module); - if (clcf->keepalive_timeout == 0 + if (ngx_exiting + || clcf->keepalive_timeout == 0 || h2c->connection->requests >= clcf->keepalive_requests || ngx_current_msec - h2c->connection->start_time > clcf->keepalive_time) What do you think? -- Maxim Dounin http://mdounin.ru/ From kasei at kasei.im Fri May 17 08:35:57 2024 From: kasei at kasei.im (Kasei Wang) Date: Fri, 17 May 2024 16:35:57 +0800 Subject: [PATCH]HTTP/2 connection not properly closing during graceful shutdown In-Reply-To: References: <0255e7345ffdebf8081563334ed7c931718d2c19.camel@kasei.im> Message-ID: Hello! On Fri, May 17, 2024 at 2:06?AM Maxim Dounin wrote: > > Hello! > > On Wed, May 15, 2024 at 11:46:29AM +0800, Kasei Wang wrote: > > > Hello, > > > > I found that there is a slight probability of HTTP/2 connections not > > properly closing during graceful shutdown, leading to worker processes > > in shutting down state remaining stuck for an extended period. After > > investigation, the issue appears to stem from the following: > > > > 1. worker processes in shutting down state use > > ngx_close_idle_connections to close all idle connections, including > > HTTP/2 connections. > > 2. For HTTP/2 connections, c->idle is set to true in ngx_http_v2_init. > > According to the explanation in > > , GOAWAY should be sent to > > all HTTP/2 connections. > > 3. There might be a time gap between ngx_event_accept and > > ngx_http_v2_init. For TLS connections, ngx_http_v2_init will be > > executed after ALPN received, and for plaintext http2 connections, > > ngx_http_v2_init will be executed after parsing the http2 preface. If > > ngx_close_idle_connections is executed between ngx_event_accept and > > ngx_http_v2_init, there's a possibility that c->idle of some > > connections is set to true AFTER ngx_close_idle_connections, causing > > those connections to not enter the GOAWAY process and leading to the > > aforementioned problem. > > > > To verify this, I've written a simple HTTP/2 client. This program will > > wait 15 seconds after TCP connection establishment before starting to > > send data. The purpose of sleep is to to raise the probability of > > encountering the issue. You can reproduce the problem by executing > > "nginx -s reload" during this 15-second wait. If you're interested, you > > can try my test program > > () to reproduce > > the issue. > > > > The following patch would call ngx_http_v2_finalize_connection to close > > http2 connections which is initialized after > > ngx_close_idle_connections. > > > > And here are some previous discussion on the another maillist: > > > > > > > > Please confirm if this issue exists, review my analysis and the patch > > if possible. Thank you very much. > > I think your analysis is correct. Thanks for catching this. > > > # HG changeset patch > > # User Kasei Wang > > # Date 1715744317 -28800 > > # Wed May 15 11:38:37 2024 +0800 > > # Node ID 7f18358cc012039f6bb4e502a6f39a7bd50d669b > > # Parent 4cfd64a8330dd5bacd839796732fd8c49d87e86e > > HTTP/2: close http2 connections initialized during graceful shutdown. > > > > In some rare cases, a HTTP/2 connections can be initialized during a > > graceful shutdown. Now close such an connection to avoid unexcepted > > delays in the graceful shutdown. > > > > diff -r 4cfd64a8330d -r 7f18358cc012 src/http/v2/ngx_http_v2.c > > --- a/src/http/v2/ngx_http_v2.c Tue May 14 17:47:44 2024 +0300 > > +++ b/src/http/v2/ngx_http_v2.c Wed May 15 11:38:37 2024 +0800 > > @@ -304,6 +304,11 @@ > > c->idle = 1; > > ngx_reusable_connection(c, 0); > > > > + if (ngx_exiting) { > > + ngx_http_v2_finalize_connection(h2c, NGX_HTTP_V2_NO_ERROR); > > + return; > > + } > > + > > if (c->buffer) { > > p = c->buffer->pos; > > end = c->buffer->last; > > The patch looks working, though in the case in question the code > will do various otherwise unneeded things, notably will send > SETTINGS and WINDOW_UPDATE frames to the client. OTOH, avoiding > these might be tricky and will require more complex changes, and > probably don't worth the effort. > > Another approach might be to allow just one request if a > connection is established just before the shutdown, much like > HTTP/1.x code does. Something as simple as the following patch > should do the trick: > > diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c > --- a/src/http/v2/ngx_http_v2.c > +++ b/src/http/v2/ngx_http_v2.c > @@ -1336,7 +1336,8 @@ ngx_http_v2_state_headers(ngx_http_v2_co > clcf = ngx_http_get_module_loc_conf(h2c->http_connection->conf_ctx, > ngx_http_core_module); > > - if (clcf->keepalive_timeout == 0 > + if (ngx_exiting > + || clcf->keepalive_timeout == 0 > || h2c->connection->requests >= clcf->keepalive_requests > || ngx_current_msec - h2c->connection->start_time > > clcf->keepalive_time) > > > What do you think? > > -- > Maxim Dounin > http://mdounin.ru/ > -- > nginx-devel mailing list > nginx-devel at freenginx.org > https://freenginx.org/mailman/listinfo/nginx-devel Thanks for your response. Actually, I was worried that sending GOAWAY on stream 0 might cause compatibility issues for some clients. There are so many client implementations, and I can't be sure that all clients can correctly retry the request in this situation. The disadvantage of your approach is that the worker process may still need to wait for the first request to arrive. This is not a very long time, and it should be limited by the client_header_timeout, which is 60s by default. So I think the waiting is acceptable. The advantage of this approach is that the process to send GOAWAY frame would be more similar to the existing reload process, reducing the possibility of affecting client compatibility. Overall, I prefer your approach because it minimizes the change impact for the client. From mdounin at mdounin.ru Tue May 21 01:16:05 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Tue, 21 May 2024 04:16:05 +0300 Subject: [nginx] Add support for XOAUTH2 and OAUTHBEARER authentication In-Reply-To: References: Message-ID: Hello! On Mon, May 13, 2024 at 03:41:45PM +1000, Robert Mueller wrote: > # HG changeset patch > # User Rob Mueller > Add support for XOAUTH2 and OAUTHBEARER authentication > > This patch adds support for RFC 7628 OAUTHBEARER style authentication > to the nginx mail proxy module for IMAP, POP and SMTP. To help with > legacy libraries, it also implements the pre-RFC XOAUTH2 protocol as well. > > It adds ngx_*_auth_{oauthbearer,xoauth2} states, constants, handlers, etc. > The bearer token provided by the client is passed to the backend auth > server in the `Auth-Pass` header. > > The auth server may return an additional optional response > header `Auth-Error-Sasl`. It's expected in the auth failure > case that the backend auth server will generate a base64 > encoded JSON object that conforms to the error reporting in > https://datatracker.ietf.org/doc/html/rfc7628#section-3.2.2 in this > header. > > If present, the value in this header is prefixed with a `+ ` (IMAP) or > `334 ` (SMTP or POP) and returned as the SASL response. We then wait for > a valid line from the client (which we ignore) and then we exit the SASL > mode and return back to standard protocol parsing. If we don't receive > a valid line, we terminate the connection. There's an example of this > looks like in https://datatracker.ietf.org/doc/html/rfc7628#section-4.3 [...] Thanks for the patch. Some problem as identified during review, in no particular order: - SASL errors are returned incorrectly for POP3 (the code uses SMTP-style "334 " challenge prefix instead of "+ " to be used in POP3, see RFC 5034). - The code maintains auth http context while auth sleep handler, which is not really needed: it is only used for a flag ("SASL response is being sent") and a pointer to non-SASL error response. - Logic in auth_http looks overcomplicated, just "use Auth-Error-SASL if its available and returning error" should be enough, much like we do for the Auth-Error-Code header in SMTP. - Without the Auth-Wait header, but with Auth-Error-SASL, the code will use ngx_add_timer(ev, 0). While this works, this is suboptimal. - Without the Auth-Wait header, but with Auth-Error-SASL, the connection won't be closed when the dummy response is received. This makes it impossible to correctly return OAUTHBEARER error without allowing yet another authentication attempt. - Various issues with dummy response waiting: - No timeouts are set while waiting for the dummy response ("AQ==" or an empty line), a DoS vector. - The dummy response waiting code does not support pipelining. - The dummy response waiting code does not recognize bare LF as a line terminator. - The dummy response waiting code closes connection on any unexpected input from the client without any diagnostic. - Formally, SASL abort might require different response codes. Notably, in IMAP server must respond to "*" with "BAD", not "NO" (note that examples in RFC 7628 are broken). Overall, an approach taken to implement dummy response waiting seems to correlate with the number of various issues introduced. A better approach might be to implement dummy response waiting in protocol-specific code, such as in the ngx_mail_imap_parse_command() function. - If disabled, XOAUTH2 and OAUTHBEARER mechanisms are still allowed in the form with initial response. Likely copied from the EXTERNAL mechanism, which contains the same bug (patch below). - The s->login field needs to be cleared by the mechanism (similarly, likely copied from the EXTERNAL mechanism which doesn't clear s->passwd, patch below). - Since s->login is not provided for the mechanisms, auth_http probably needs to check if it is returned by the auth script, similarly to s->passwd. Below are patches to address identified issues in the EXTERNAL mechanism and a reworked XOAUTH2/OAUTHBEARER patch, which tries to address issues mentioned above. In particular, waiting for the dummy SASL response is implemented by returning to the auth state without clearing the state, so mechanisms can simply continue handling additional SASL responses. Please take a look. Additionally, I tend to think that a better approach to communicate with the auth server might be to parse SASL initial responses as sent by clients, and provide parsed information, username and the Bearer token, in the "Auth-User" and "Auth-Pass" headers, similarly to what we do for other SASL mechanism, such as PLAIN and CRAM-MD5. This will certainly work for XOAUTH2, since the only information is the username and the Bearer token. In OAUTHBEARER some additional information can be provided, but I very much doubt it is used in practice (and, if needed, full initial response can be sent separately, in a separate header). And parsing implementation seems to be easy enough. Immediate benefits include early correctness checking (e.g., "n,user=test at example.com,^A..." as used in tests and in some RFC 7628 examples is incorrect, it should be "n,a=test at example.com,^A..." instead) and simpler auth server implementation. I've provided proof-of-concept as a separate patch below. What do you think about this approach? Patches for the code: # HG changeset patch # User Maxim Dounin # Date 1716247666 -10800 # Tue May 21 02:27:46 2024 +0300 # Node ID e6aefee77e6c667412977b6af3fdb87aff73e514 # Parent 46ecad404a296042c0088e699f275a92758e5ab9 Mail: fixed EXTERNAL to be accepted only if enabled. As originally implemented in 6774:bcb107bb89cd, it wasn't possible to disable the EXTERNAL authentication method: it was always accepted (but not advertised unless enabled). It is, however, believed that it is better to reject attempts to use the disabled method, hence in 6869:b2915d99ee8d an attempt was made to address this. This attempt was insufficient though: it was still possible to use the method as long as initial SASL response was used. With this patch both challenge-response and initial response forms are disabled. Additionally, initial response handling for the PLAIN authentication is removed from ngx_mail_auth_parse(), for consistency and to don't provoke such bugs. diff --git a/src/mail/ngx_mail_imap_handler.c b/src/mail/ngx_mail_imap_handler.c --- a/src/mail/ngx_mail_imap_handler.c +++ b/src/mail/ngx_mail_imap_handler.c @@ -388,6 +388,10 @@ ngx_mail_imap_authenticate(ngx_mail_sess case NGX_MAIL_AUTH_PLAIN: + if (s->args.nelts == 2) { + return ngx_mail_auth_plain(s, c, 1); + } + ngx_str_set(&s->out, imap_plain_next); s->mail_state = ngx_imap_auth_plain; @@ -420,6 +424,10 @@ ngx_mail_imap_authenticate(ngx_mail_sess return NGX_MAIL_PARSE_INVALID_COMMAND; } + if (s->args.nelts == 2) { + return ngx_mail_auth_external(s, c, 1); + } + ngx_str_set(&s->out, imap_username); s->mail_state = ngx_imap_auth_external; diff --git a/src/mail/ngx_mail_parse.c b/src/mail/ngx_mail_parse.c --- a/src/mail/ngx_mail_parse.c +++ b/src/mail/ngx_mail_parse.c @@ -934,13 +934,11 @@ ngx_mail_auth_parse(ngx_mail_session_t * if (ngx_strncasecmp(arg[0].data, (u_char *) "PLAIN", 5) == 0) { - if (s->args.nelts == 1) { + if (s->args.nelts == 1 || s->args.nelts == 2) { return NGX_MAIL_AUTH_PLAIN; } - if (s->args.nelts == 2) { - return ngx_mail_auth_plain(s, c, 1); - } + return NGX_MAIL_PARSE_INVALID_COMMAND; } return NGX_MAIL_PARSE_INVALID_COMMAND; @@ -959,13 +957,11 @@ ngx_mail_auth_parse(ngx_mail_session_t * if (ngx_strncasecmp(arg[0].data, (u_char *) "EXTERNAL", 8) == 0) { - if (s->args.nelts == 1) { + if (s->args.nelts == 1 || s->args.nelts == 2) { return NGX_MAIL_AUTH_EXTERNAL; } - if (s->args.nelts == 2) { - return ngx_mail_auth_external(s, c, 1); - } + return NGX_MAIL_PARSE_INVALID_COMMAND; } return NGX_MAIL_PARSE_INVALID_COMMAND; diff --git a/src/mail/ngx_mail_pop3_handler.c b/src/mail/ngx_mail_pop3_handler.c --- a/src/mail/ngx_mail_pop3_handler.c +++ b/src/mail/ngx_mail_pop3_handler.c @@ -517,6 +517,10 @@ ngx_mail_pop3_auth(ngx_mail_session_t *s case NGX_MAIL_AUTH_PLAIN: + if (s->args.nelts == 2) { + return ngx_mail_auth_plain(s, c, 1); + } + ngx_str_set(&s->out, pop3_next); s->mail_state = ngx_pop3_auth_plain; @@ -541,6 +545,10 @@ ngx_mail_pop3_auth(ngx_mail_session_t *s return NGX_MAIL_PARSE_INVALID_COMMAND; } + if (s->args.nelts == 2) { + return ngx_mail_auth_external(s, c, 1); + } + ngx_str_set(&s->out, pop3_username); s->mail_state = ngx_pop3_auth_external; diff --git a/src/mail/ngx_mail_smtp_handler.c b/src/mail/ngx_mail_smtp_handler.c --- a/src/mail/ngx_mail_smtp_handler.c +++ b/src/mail/ngx_mail_smtp_handler.c @@ -701,6 +701,10 @@ ngx_mail_smtp_auth(ngx_mail_session_t *s case NGX_MAIL_AUTH_PLAIN: + if (s->args.nelts == 2) { + return ngx_mail_auth_plain(s, c, 1); + } + ngx_str_set(&s->out, smtp_next); s->mail_state = ngx_smtp_auth_plain; @@ -733,6 +737,10 @@ ngx_mail_smtp_auth(ngx_mail_session_t *s return NGX_MAIL_PARSE_INVALID_COMMAND; } + if (s->args.nelts == 2) { + return ngx_mail_auth_external(s, c, 1); + } + ngx_str_set(&s->out, smtp_username); s->mail_state = ngx_smtp_auth_external; # HG changeset patch # User Maxim Dounin # Date 1716247668 -10800 # Tue May 21 02:27:48 2024 +0300 # Node ID 6dbc239d5e2bdbf69baa7d4cbdfa3871cfe67bbf # Parent e6aefee77e6c667412977b6af3fdb87aff73e514 Mail: fixed EXTERNAL auth to clear s->passwd. The s->passwd field might be set after previous (failed) authentication in the same session, and since EXTERNAL authentication did not touch it, it was sent to the auth server. diff --git a/src/mail/ngx_mail_handler.c b/src/mail/ngx_mail_handler.c --- a/src/mail/ngx_mail_handler.c +++ b/src/mail/ngx_mail_handler.c @@ -744,6 +744,8 @@ ngx_mail_auth_external(ngx_mail_session_ s->login.len = external.len; s->login.data = external.data; + ngx_str_null(&s->passwd); + ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0, "mail auth external: \"%V\"", &s->login); # HG changeset patch # User Maxim Dounin # Date 1716247671 -10800 # Tue May 21 02:27:51 2024 +0300 # Node ID 5692eed9d5f4d7b53e3d2eb377a3249466a23000 # Parent 6dbc239d5e2bdbf69baa7d4cbdfa3871cfe67bbf Mail: added some parsing debug logging. diff --git a/src/mail/ngx_mail_parse.c b/src/mail/ngx_mail_parse.c --- a/src/mail/ngx_mail_parse.c +++ b/src/mail/ngx_mail_parse.c @@ -30,6 +30,9 @@ ngx_mail_pop3_parse_command(ngx_mail_ses state = s->state; + ngx_log_debug1(NGX_LOG_DEBUG_MAIL, s->connection->log, 0, + "pop3 parse: %d", state); + for (p = s->buffer->pos; p < s->buffer->last; p++) { ch = *p; @@ -248,6 +251,9 @@ ngx_mail_imap_parse_command(ngx_mail_ses state = s->state; + ngx_log_debug1(NGX_LOG_DEBUG_MAIL, s->connection->log, 0, + "imap parse: %d", state); + for (p = s->buffer->pos; p < s->buffer->last; p++) { ch = *p; @@ -692,6 +698,9 @@ ngx_mail_smtp_parse_command(ngx_mail_ses state = s->state; + ngx_log_debug1(NGX_LOG_DEBUG_MAIL, s->connection->log, 0, + "smtp parse: %d", state); + for (p = s->buffer->pos; p < s->buffer->last; p++) { ch = *p; # HG changeset patch # User Maxim Dounin # Date 1716247679 -10800 # Tue May 21 02:27:59 2024 +0300 # Node ID b06a347640e565012aced65e2a694a306ed2db5c # Parent 5692eed9d5f4d7b53e3d2eb377a3249466a23000 Mail: added support for XOAUTH2 and OAUTHBEARER authentication. This patch adds support for the OAUTHBEARER SASL mechanism as defined by RFC 7628, as well as pre-RFC XOAUTH2 SASL mechanism. For both mechanisms, full initial SASL response as sent by the client is passed to the auth server in the "Auth-Pass" header, base64-decoded. The auth server may return the "Auth-Error-SASL" header, which is passed to the client as an additional SASL challenge. It is expected to contain mechanism-specific error details, base64-encoded. After clients responds (with an empty SASL response for XAUTH2, or with "AQ==" dummy response for OAUTHBEARER), the error message from the "Auth-Status" header is sent. Based on a patch by Rob Mueller. diff --git a/src/mail/ngx_mail.h b/src/mail/ngx_mail.h --- a/src/mail/ngx_mail.h +++ b/src/mail/ngx_mail.h @@ -141,7 +141,9 @@ typedef enum { ngx_pop3_auth_login_password, ngx_pop3_auth_plain, ngx_pop3_auth_cram_md5, - ngx_pop3_auth_external + ngx_pop3_auth_external, + ngx_pop3_auth_xoauth2, + ngx_pop3_auth_oauthbearer } ngx_pop3_state_e; @@ -152,6 +154,8 @@ typedef enum { ngx_imap_auth_plain, ngx_imap_auth_cram_md5, ngx_imap_auth_external, + ngx_imap_auth_xoauth2, + ngx_imap_auth_oauthbearer, ngx_imap_login, ngx_imap_user, ngx_imap_passwd @@ -165,6 +169,8 @@ typedef enum { ngx_smtp_auth_plain, ngx_smtp_auth_cram_md5, ngx_smtp_auth_external, + ngx_smtp_auth_xoauth2, + ngx_smtp_auth_oauthbearer, ngx_smtp_helo, ngx_smtp_helo_xclient, ngx_smtp_helo_auth, @@ -212,8 +218,9 @@ typedef struct { unsigned no_sync_literal:1; unsigned starttls:1; unsigned esmtp:1; - unsigned auth_method:3; + unsigned auth_method:4; unsigned auth_wait:1; + unsigned auth_quit:1; ngx_str_t login; ngx_str_t passwd; @@ -229,6 +236,8 @@ typedef struct { ngx_str_t smtp_from; ngx_str_t smtp_to; + ngx_str_t auth_err; + ngx_str_t cmd; ngx_uint_t command; @@ -303,15 +312,19 @@ typedef struct { #define NGX_MAIL_AUTH_APOP 3 #define NGX_MAIL_AUTH_CRAM_MD5 4 #define NGX_MAIL_AUTH_EXTERNAL 5 -#define NGX_MAIL_AUTH_NONE 6 +#define NGX_MAIL_AUTH_XOAUTH2 6 +#define NGX_MAIL_AUTH_OAUTHBEARER 7 +#define NGX_MAIL_AUTH_NONE 8 -#define NGX_MAIL_AUTH_PLAIN_ENABLED 0x0002 -#define NGX_MAIL_AUTH_LOGIN_ENABLED 0x0004 -#define NGX_MAIL_AUTH_APOP_ENABLED 0x0008 -#define NGX_MAIL_AUTH_CRAM_MD5_ENABLED 0x0010 -#define NGX_MAIL_AUTH_EXTERNAL_ENABLED 0x0020 -#define NGX_MAIL_AUTH_NONE_ENABLED 0x0040 +#define NGX_MAIL_AUTH_PLAIN_ENABLED 0x0002 +#define NGX_MAIL_AUTH_LOGIN_ENABLED 0x0004 +#define NGX_MAIL_AUTH_APOP_ENABLED 0x0008 +#define NGX_MAIL_AUTH_CRAM_MD5_ENABLED 0x0010 +#define NGX_MAIL_AUTH_EXTERNAL_ENABLED 0x0020 +#define NGX_MAIL_AUTH_XOAUTH2_ENABLED 0x0040 +#define NGX_MAIL_AUTH_OAUTHBEARER_ENABLED 0x0080 +#define NGX_MAIL_AUTH_NONE_ENABLED 0x0100 #define NGX_MAIL_PARSE_INVALID_COMMAND 20 @@ -399,6 +412,8 @@ ngx_int_t ngx_mail_auth_cram_md5_salt(ng ngx_int_t ngx_mail_auth_cram_md5(ngx_mail_session_t *s, ngx_connection_t *c); ngx_int_t ngx_mail_auth_external(ngx_mail_session_t *s, ngx_connection_t *c, ngx_uint_t n); +ngx_int_t ngx_mail_auth_oauth(ngx_mail_session_t *s, ngx_connection_t *c, + ngx_uint_t n, ngx_uint_t auth_method); ngx_int_t ngx_mail_auth_parse(ngx_mail_session_t *s, ngx_connection_t *c); void ngx_mail_send(ngx_event_t *wev); diff --git a/src/mail/ngx_mail_auth_http_module.c b/src/mail/ngx_mail_auth_http_module.c --- a/src/mail/ngx_mail_auth_http_module.c +++ b/src/mail/ngx_mail_auth_http_module.c @@ -53,6 +53,7 @@ struct ngx_mail_auth_http_ctx_s { ngx_str_t err; ngx_str_t errmsg; ngx_str_t errcode; + ngx_str_t errsasl; time_t sleep; @@ -67,6 +68,7 @@ static void ngx_mail_auth_http_ignore_st static void ngx_mail_auth_http_process_headers(ngx_mail_session_t *s, ngx_mail_auth_http_ctx_t *ctx); static void ngx_mail_auth_sleep_handler(ngx_event_t *rev); +static void ngx_mail_auth_send_error(ngx_mail_session_t *s); static ngx_int_t ngx_mail_auth_http_parse_header_line(ngx_mail_session_t *s, ngx_mail_auth_http_ctx_t *ctx); static void ngx_mail_auth_http_block_read(ngx_event_t *rev); @@ -152,6 +154,8 @@ static ngx_str_t ngx_mail_auth_http_me ngx_string("apop"), ngx_string("cram-md5"), ngx_string("external"), + ngx_string("xoauth2"), + ngx_string("oauthbearer"), ngx_string("none") }; @@ -677,6 +681,51 @@ ngx_mail_auth_http_process_headers(ngx_m continue; } + if (len == sizeof("Auth-Error-SASL") - 1 + && ngx_strncasecmp(ctx->header_name_start, + (u_char *) "Auth-Error-SASL", + sizeof("Auth-Error-SASL") - 1) + == 0) + { + if (s->auth_method != NGX_MAIL_AUTH_XOAUTH2 + && s->auth_method != NGX_MAIL_AUTH_OAUTHBEARER) + { + continue; + } + + len = ctx->header_end - ctx->header_start; + + if (s->protocol == NGX_MAIL_SMTP_PROTOCOL) { + size = len + sizeof("334 " CRLF) - 1; + + } else { + size = len + sizeof("+ " CRLF) - 1; + } + + p = ngx_pnalloc(s->connection->pool, size); + if (p == NULL) { + ngx_close_connection(ctx->peer.connection); + ngx_destroy_pool(ctx->pool); + ngx_mail_session_internal_server_error(s); + return; + } + + ctx->errsasl.len = size; + ctx->errsasl.data = p; + + if (s->protocol == NGX_MAIL_SMTP_PROTOCOL) { + *p++ = '3'; *p++ = '3'; *p++ = '4'; *p++ = ' '; + + } else { + *p++ = '+'; *p++ = ' '; + } + + p = ngx_cpymem(p, ctx->header_start, len); + *p++ = CR; *p = LF; + + continue; + } + /* ignore other headers */ continue; @@ -717,14 +766,15 @@ ngx_mail_auth_http_process_headers(ngx_m *p++ = CR; *p = LF; } - s->out = ctx->err; + s->out = ctx->errsasl; + s->auth_err = ctx->err; timer = ctx->sleep; ngx_destroy_pool(ctx->pool); if (timer == 0) { - s->quit = 1; - ngx_mail_send(s->connection->write); + s->auth_quit = 1; + ngx_mail_auth_send_error(s); return; } @@ -772,6 +822,17 @@ ngx_mail_auth_http_process_headers(ngx_m return; } + if (s->login.data == NULL + && s->protocol != NGX_MAIL_SMTP_PROTOCOL) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "auth http server %V did not send username", + ctx->peer.name); + ngx_destroy_pool(ctx->pool); + ngx_mail_session_internal_server_error(s); + return; + } + peer = ngx_pcalloc(s->connection->pool, sizeof(ngx_addr_t)); if (peer == NULL) { ngx_destroy_pool(ctx->pool); @@ -858,9 +919,8 @@ ngx_mail_auth_http_process_headers(ngx_m static void ngx_mail_auth_sleep_handler(ngx_event_t *rev) { - ngx_connection_t *c; - ngx_mail_session_t *s; - ngx_mail_core_srv_conf_t *cscf; + ngx_connection_t *c; + ngx_mail_session_t *s; ngx_log_debug0(NGX_LOG_DEBUG_MAIL, rev->log, 0, "mail auth sleep handler"); @@ -877,33 +937,7 @@ ngx_mail_auth_sleep_handler(ngx_event_t return; } - cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module); - - rev->handler = cscf->protocol->auth_state; - - s->mail_state = 0; - s->auth_method = NGX_MAIL_AUTH_PLAIN; - s->tag.len = 0; - - c->log->action = "in auth state"; - - ngx_mail_send(c->write); - - if (c->destroyed) { - return; - } - - ngx_add_timer(rev, cscf->timeout); - - if (rev->ready) { - rev->handler(rev); - return; - } - - if (ngx_handle_read_event(rev, 0) != NGX_OK) { - ngx_mail_close_connection(c); - } - + ngx_mail_auth_send_error(s); return; } @@ -915,6 +949,57 @@ ngx_mail_auth_sleep_handler(ngx_event_t } +static void +ngx_mail_auth_send_error(ngx_mail_session_t *s) +{ + ngx_event_t *rev; + ngx_connection_t *c; + ngx_mail_core_srv_conf_t *cscf; + + c = s->connection; + rev = c->read; + + cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module); + + rev->handler = cscf->protocol->auth_state; + + s->auth_method = NGX_MAIL_AUTH_PLAIN; + + c->log->action = "in auth state"; + + if (s->out.len == 0) { + s->out = s->auth_err; + s->quit = s->auth_quit; + ngx_str_null(&s->auth_err); + + s->state = 0; + s->mail_state = 0; + s->tag.len = 0; + + } else { + s->auth_err.len -= s->tag.len; + s->auth_err.data += s->tag.len; + } + + ngx_mail_send(c->write); + + if (c->destroyed) { + return; + } + + ngx_add_timer(rev, cscf->timeout); + + if (rev->ready) { + rev->handler(rev); + return; + } + + if (ngx_handle_read_event(rev, 0) != NGX_OK) { + ngx_mail_close_connection(c); + } +} + + static ngx_int_t ngx_mail_auth_http_parse_header_line(ngx_mail_session_t *s, ngx_mail_auth_http_ctx_t *ctx) diff --git a/src/mail/ngx_mail_handler.c b/src/mail/ngx_mail_handler.c --- a/src/mail/ngx_mail_handler.c +++ b/src/mail/ngx_mail_handler.c @@ -755,6 +755,70 @@ ngx_mail_auth_external(ngx_mail_session_ } +ngx_int_t +ngx_mail_auth_oauth(ngx_mail_session_t *s, ngx_connection_t *c, + ngx_uint_t n, ngx_uint_t auth_method) +{ + ngx_str_t *arg, oauth; + + arg = s->args.elts; + + if (s->auth_err.len) { + ngx_log_debug0(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth oauth cancel"); + + if (s->args.nelts != 1) { + goto invalid; + } + + if (arg[0].len == 0 + || ngx_strncmp(arg[0].data, (u_char *) "AQ==", 4) == 0) + { + s->out = s->auth_err; + s->quit = s->auth_quit; + s->state = 0; + s->mail_state = 0; + ngx_str_null(&s->auth_err); + return NGX_OK; + } + + invalid: + + s->quit = s->auth_quit; + ngx_str_null(&s->auth_err); + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth oauth: \"%V\" type %ui", &arg[n], auth_method); + + oauth.data = ngx_pnalloc(c->pool, ngx_base64_decoded_length(arg[n].len)); + if (oauth.data == NULL) { + return NGX_ERROR; + } + + if (ngx_decode_base64(&oauth, &arg[n]) != NGX_OK) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid base64 encoding in " + "AUTH XOAUTH2/OAUTHBEARER command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + s->passwd.len = oauth.len; + s->passwd.data = oauth.data; + + ngx_str_null(&s->login); + + ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth oauth: \"%V\"", &s->passwd); + + s->auth_method = auth_method; + + return NGX_DONE; +} + + void ngx_mail_send(ngx_event_t *wev) { @@ -919,13 +983,17 @@ ngx_mail_auth(ngx_mail_session_t *s, ngx { s->args.nelts = 0; - if (s->buffer->pos == s->buffer->last) { - s->buffer->pos = s->buffer->start; - s->buffer->last = s->buffer->start; + if (s->state) { + /* preserve tag */ + s->arg_start = s->buffer->pos; + + } else { + if (s->buffer->pos == s->buffer->last) { + s->buffer->pos = s->buffer->start; + s->buffer->last = s->buffer->start; + } } - s->state = 0; - if (c->read->timer_set) { ngx_del_timer(c->read); } diff --git a/src/mail/ngx_mail_imap_handler.c b/src/mail/ngx_mail_imap_handler.c --- a/src/mail/ngx_mail_imap_handler.c +++ b/src/mail/ngx_mail_imap_handler.c @@ -220,6 +220,14 @@ ngx_mail_imap_auth_state(ngx_event_t *re case ngx_imap_auth_external: rc = ngx_mail_auth_external(s, c, 0); break; + + case ngx_imap_auth_xoauth2: + rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_XOAUTH2); + break; + + case ngx_imap_auth_oauthbearer: + rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_OAUTHBEARER); + break; } } else if (rc == NGX_IMAP_NEXT) { @@ -432,6 +440,38 @@ ngx_mail_imap_authenticate(ngx_mail_sess s->mail_state = ngx_imap_auth_external; return NGX_OK; + + case NGX_MAIL_AUTH_XOAUTH2: + + if (!(iscf->auth_methods & NGX_MAIL_AUTH_XOAUTH2_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_imap_auth_xoauth2; + return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_XOAUTH2); + } + + ngx_str_set(&s->out, imap_plain_next); + s->mail_state = ngx_imap_auth_xoauth2; + + return NGX_OK; + + case NGX_MAIL_AUTH_OAUTHBEARER: + + if (!(iscf->auth_methods & NGX_MAIL_AUTH_OAUTHBEARER_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_imap_auth_oauthbearer; + return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_OAUTHBEARER); + } + + ngx_str_set(&s->out, imap_plain_next); + s->mail_state = ngx_imap_auth_oauthbearer; + + return NGX_OK; } return rc; diff --git a/src/mail/ngx_mail_imap_module.c b/src/mail/ngx_mail_imap_module.c --- a/src/mail/ngx_mail_imap_module.c +++ b/src/mail/ngx_mail_imap_module.c @@ -30,6 +30,8 @@ static ngx_conf_bitmask_t ngx_mail_imap { ngx_string("login"), NGX_MAIL_AUTH_LOGIN_ENABLED }, { ngx_string("cram-md5"), NGX_MAIL_AUTH_CRAM_MD5_ENABLED }, { ngx_string("external"), NGX_MAIL_AUTH_EXTERNAL_ENABLED }, + { ngx_string("xoauth2"), NGX_MAIL_AUTH_XOAUTH2_ENABLED }, + { ngx_string("oauthbearer"), NGX_MAIL_AUTH_OAUTHBEARER_ENABLED }, { ngx_null_string, 0 } }; @@ -40,6 +42,8 @@ static ngx_str_t ngx_mail_imap_auth_met ngx_null_string, /* APOP */ ngx_string("AUTH=CRAM-MD5"), ngx_string("AUTH=EXTERNAL"), + ngx_string("AUTH=XOAUTH2"), + ngx_string("AUTH=OAUTHBEARER"), ngx_null_string /* NONE */ }; @@ -182,7 +186,7 @@ ngx_mail_imap_merge_srv_conf(ngx_conf_t } for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { @@ -208,7 +212,7 @@ ngx_mail_imap_merge_srv_conf(ngx_conf_t auth = p; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { diff --git a/src/mail/ngx_mail_parse.c b/src/mail/ngx_mail_parse.c --- a/src/mail/ngx_mail_parse.c +++ b/src/mail/ngx_mail_parse.c @@ -953,6 +953,20 @@ ngx_mail_auth_parse(ngx_mail_session_t * return NGX_MAIL_PARSE_INVALID_COMMAND; } + if (arg[0].len == 7) { + + if (ngx_strncasecmp(arg[0].data, (u_char *) "XOAUTH2", 7) == 0) { + + if (s->args.nelts == 1 || s->args.nelts == 2) { + return NGX_MAIL_AUTH_XOAUTH2; + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + if (arg[0].len == 8) { if (ngx_strncasecmp(arg[0].data, (u_char *) "CRAM-MD5", 8) == 0) { @@ -976,5 +990,19 @@ ngx_mail_auth_parse(ngx_mail_session_t * return NGX_MAIL_PARSE_INVALID_COMMAND; } + if (arg[0].len == 11) { + + if (ngx_strncasecmp(arg[0].data, (u_char *) "OAUTHBEARER", 11) == 0) { + + if (s->args.nelts == 1 || s->args.nelts == 2) { + return NGX_MAIL_AUTH_OAUTHBEARER; + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + return NGX_MAIL_PARSE_INVALID_COMMAND; } diff --git a/src/mail/ngx_mail_pop3_handler.c b/src/mail/ngx_mail_pop3_handler.c --- a/src/mail/ngx_mail_pop3_handler.c +++ b/src/mail/ngx_mail_pop3_handler.c @@ -260,6 +260,14 @@ ngx_mail_pop3_auth_state(ngx_event_t *re case ngx_pop3_auth_external: rc = ngx_mail_auth_external(s, c, 0); break; + + case ngx_pop3_auth_xoauth2: + rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_XOAUTH2); + break; + + case ngx_pop3_auth_oauthbearer: + rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_OAUTHBEARER); + break; } } @@ -553,6 +561,38 @@ ngx_mail_pop3_auth(ngx_mail_session_t *s s->mail_state = ngx_pop3_auth_external; return NGX_OK; + + case NGX_MAIL_AUTH_XOAUTH2: + + if (!(pscf->auth_methods & NGX_MAIL_AUTH_XOAUTH2_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_pop3_auth_xoauth2; + return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_XOAUTH2); + } + + ngx_str_set(&s->out, pop3_next); + s->mail_state = ngx_pop3_auth_xoauth2; + + return NGX_OK; + + case NGX_MAIL_AUTH_OAUTHBEARER: + + if (!(pscf->auth_methods & NGX_MAIL_AUTH_OAUTHBEARER_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_pop3_auth_oauthbearer; + return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_OAUTHBEARER); + } + + ngx_str_set(&s->out, pop3_next); + s->mail_state = ngx_pop3_auth_oauthbearer; + + return NGX_OK; } return rc; diff --git a/src/mail/ngx_mail_pop3_module.c b/src/mail/ngx_mail_pop3_module.c --- a/src/mail/ngx_mail_pop3_module.c +++ b/src/mail/ngx_mail_pop3_module.c @@ -30,6 +30,8 @@ static ngx_conf_bitmask_t ngx_mail_pop3 { ngx_string("apop"), NGX_MAIL_AUTH_APOP_ENABLED }, { ngx_string("cram-md5"), NGX_MAIL_AUTH_CRAM_MD5_ENABLED }, { ngx_string("external"), NGX_MAIL_AUTH_EXTERNAL_ENABLED }, + { ngx_string("xoauth2"), NGX_MAIL_AUTH_XOAUTH2_ENABLED }, + { ngx_string("oauthbearer"), NGX_MAIL_AUTH_OAUTHBEARER_ENABLED }, { ngx_null_string, 0 } }; @@ -40,6 +42,8 @@ static ngx_str_t ngx_mail_pop3_auth_met ngx_null_string, /* APOP */ ngx_string("CRAM-MD5"), ngx_string("EXTERNAL"), + ngx_string("XOAUTH2"), + ngx_string("OAUTHBEARER"), ngx_null_string /* NONE */ }; @@ -183,7 +187,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t size += sizeof("SASL") - 1 + sizeof(CRLF) - 1; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { @@ -214,7 +218,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t p = ngx_cpymem(p, "SASL", sizeof("SASL") - 1); for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { @@ -254,7 +258,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t + sizeof("." CRLF) - 1; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { @@ -279,7 +283,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t sizeof("+OK methods supported:" CRLF) - 1); for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { diff --git a/src/mail/ngx_mail_smtp_handler.c b/src/mail/ngx_mail_smtp_handler.c --- a/src/mail/ngx_mail_smtp_handler.c +++ b/src/mail/ngx_mail_smtp_handler.c @@ -548,6 +548,14 @@ ngx_mail_smtp_auth_state(ngx_event_t *re case ngx_smtp_auth_external: rc = ngx_mail_auth_external(s, c, 0); break; + + case ngx_smtp_auth_xoauth2: + rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_XOAUTH2); + break; + + case ngx_smtp_auth_oauthbearer: + rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_OAUTHBEARER); + break; } } @@ -745,6 +753,38 @@ ngx_mail_smtp_auth(ngx_mail_session_t *s s->mail_state = ngx_smtp_auth_external; return NGX_OK; + + case NGX_MAIL_AUTH_XOAUTH2: + + if (!(sscf->auth_methods & NGX_MAIL_AUTH_XOAUTH2_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_smtp_auth_xoauth2; + return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_XOAUTH2); + } + + ngx_str_set(&s->out, smtp_next); + s->mail_state = ngx_smtp_auth_xoauth2; + + return NGX_OK; + + case NGX_MAIL_AUTH_OAUTHBEARER: + + if (!(sscf->auth_methods & NGX_MAIL_AUTH_OAUTHBEARER_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_smtp_auth_oauthbearer; + return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_OAUTHBEARER); + } + + ngx_str_set(&s->out, smtp_next); + s->mail_state = ngx_smtp_auth_oauthbearer; + + return NGX_OK; } return rc; diff --git a/src/mail/ngx_mail_smtp_module.c b/src/mail/ngx_mail_smtp_module.c --- a/src/mail/ngx_mail_smtp_module.c +++ b/src/mail/ngx_mail_smtp_module.c @@ -22,6 +22,8 @@ static ngx_conf_bitmask_t ngx_mail_smtp { ngx_string("login"), NGX_MAIL_AUTH_LOGIN_ENABLED }, { ngx_string("cram-md5"), NGX_MAIL_AUTH_CRAM_MD5_ENABLED }, { ngx_string("external"), NGX_MAIL_AUTH_EXTERNAL_ENABLED }, + { ngx_string("xoauth2"), NGX_MAIL_AUTH_XOAUTH2_ENABLED }, + { ngx_string("oauthbearer"), NGX_MAIL_AUTH_OAUTHBEARER_ENABLED }, { ngx_string("none"), NGX_MAIL_AUTH_NONE_ENABLED }, { ngx_null_string, 0 } }; @@ -33,6 +35,8 @@ static ngx_str_t ngx_mail_smtp_auth_met ngx_null_string, /* APOP */ ngx_string("CRAM-MD5"), ngx_string("EXTERNAL"), + ngx_string("XOAUTH2"), + ngx_string("OAUTHBEARER"), ngx_null_string /* NONE */ }; @@ -210,7 +214,7 @@ ngx_mail_smtp_merge_srv_conf(ngx_conf_t auth_enabled = 0; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { @@ -253,7 +257,7 @@ ngx_mail_smtp_merge_srv_conf(ngx_conf_t *p++ = 'A'; *p++ = 'U'; *p++ = 'T'; *p++ = 'H'; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { # HG changeset patch # User Maxim Dounin # Date 1716247681 -10800 # Tue May 21 02:28:01 2024 +0300 # Node ID 377c966d4623000c0b9a5b2c0d47f9dd64b4cd9d # Parent b06a347640e565012aced65e2a694a306ed2db5c Mail: parsing of XOAUTH2 and OAUTHBEARER. For both mechanisms, the "Auth-User" header is set to the client identity obtained from the initial SASL response sent by the client, and the "Auth-Pass" header is set the Bearer token itself. Additionally, only continuation responses correct for the particular mechanism are now accepted after errors ("AQ==" for OAUTHBEARER, empty line for XOAUTH2). To be merged with the previous patch. diff --git a/src/mail/ngx_mail.h b/src/mail/ngx_mail.h --- a/src/mail/ngx_mail.h +++ b/src/mail/ngx_mail.h @@ -412,8 +412,10 @@ ngx_int_t ngx_mail_auth_cram_md5_salt(ng ngx_int_t ngx_mail_auth_cram_md5(ngx_mail_session_t *s, ngx_connection_t *c); ngx_int_t ngx_mail_auth_external(ngx_mail_session_t *s, ngx_connection_t *c, ngx_uint_t n); -ngx_int_t ngx_mail_auth_oauth(ngx_mail_session_t *s, ngx_connection_t *c, - ngx_uint_t n, ngx_uint_t auth_method); +ngx_int_t ngx_mail_auth_xoauth2(ngx_mail_session_t *s, ngx_connection_t *c, + ngx_uint_t n); +ngx_int_t ngx_mail_auth_oauthbearer(ngx_mail_session_t *s, ngx_connection_t *c, + ngx_uint_t n); ngx_int_t ngx_mail_auth_parse(ngx_mail_session_t *s, ngx_connection_t *c); void ngx_mail_send(ngx_event_t *wev); diff --git a/src/mail/ngx_mail_auth_http_module.c b/src/mail/ngx_mail_auth_http_module.c --- a/src/mail/ngx_mail_auth_http_module.c +++ b/src/mail/ngx_mail_auth_http_module.c @@ -822,17 +822,6 @@ ngx_mail_auth_http_process_headers(ngx_m return; } - if (s->login.data == NULL - && s->protocol != NGX_MAIL_SMTP_PROTOCOL) - { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "auth http server %V did not send username", - ctx->peer.name); - ngx_destroy_pool(ctx->pool); - ngx_mail_session_internal_server_error(s); - return; - } - peer = ngx_pcalloc(s->connection->pool, sizeof(ngx_addr_t)); if (peer == NULL) { ngx_destroy_pool(ctx->pool); diff --git a/src/mail/ngx_mail_handler.c b/src/mail/ngx_mail_handler.c --- a/src/mail/ngx_mail_handler.c +++ b/src/mail/ngx_mail_handler.c @@ -756,23 +756,126 @@ ngx_mail_auth_external(ngx_mail_session_ ngx_int_t -ngx_mail_auth_oauth(ngx_mail_session_t *s, ngx_connection_t *c, - ngx_uint_t n, ngx_uint_t auth_method) +ngx_mail_auth_xoauth2(ngx_mail_session_t *s, ngx_connection_t *c, ngx_uint_t n) { + u_char *p, *last; ngx_str_t *arg, oauth; arg = s->args.elts; if (s->auth_err.len) { ngx_log_debug0(NGX_LOG_DEBUG_MAIL, c->log, 0, - "mail auth oauth cancel"); + "mail auth xoauth2 cancel"); - if (s->args.nelts != 1) { - goto invalid; + if (s->args.nelts == 1 && arg[0].len == 0) { + s->out = s->auth_err; + s->quit = s->auth_quit; + s->state = 0; + s->mail_state = 0; + ngx_str_null(&s->auth_err); + return NGX_OK; } - if (arg[0].len == 0 - || ngx_strncmp(arg[0].data, (u_char *) "AQ==", 4) == 0) + s->quit = s->auth_quit; + ngx_str_null(&s->auth_err); + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth xoauth2: \"%V\"", &arg[n]); + + oauth.data = ngx_pnalloc(c->pool, ngx_base64_decoded_length(arg[n].len)); + if (oauth.data == NULL) { + return NGX_ERROR; + } + + if (ngx_decode_base64(&oauth, &arg[n]) != NGX_OK) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid base64 encoding in " + "AUTH XOAUTH2 command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + /* + * https://developers.google.com/gmail/imap/xoauth2-protocol + * "user=" {User} "^Aauth=Bearer " {token} "^A^A" + */ + + p = oauth.data; + last = p + oauth.len; + + while (p < last) { + if (*p++ == '\1') { + s->login.len = p - oauth.data - 1; + s->login.data = oauth.data; + s->passwd.len = last - p; + s->passwd.data = p; + break; + } + } + + if (s->login.len < sizeof("user=") - 1 + || ngx_strncasecmp(s->login.data, (u_char *) "user=", + sizeof("user=") - 1) + != 0) + { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid login in AUTH XOAUTH2 command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + s->login.len -= sizeof("user=") - 1; + s->login.data += sizeof("user=") - 1; + + if (s->passwd.len < sizeof("auth=Bearer ") - 1 + || ngx_strncasecmp(s->passwd.data, (u_char *) "auth=Bearer ", + sizeof("auth=Bearer ") - 1) + != 0) + { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid token in AUTH XOAUTH2 command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + s->passwd.len -= sizeof("auth=Bearer ") - 1; + s->passwd.data += sizeof("auth=Bearer ") - 1; + + if (s->passwd.len < 2 + || s->passwd.data[s->passwd.len - 2] != '\1' + || s->passwd.data[s->passwd.len - 1] != '\1') + { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid token in AUTH XOAUTH2 command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + s->passwd.len -= 2; + + ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth xoauth2: \"%V\" \"%V\"", &s->login, &s->passwd); + + s->auth_method = NGX_MAIL_AUTH_XOAUTH2; + + return NGX_DONE; +} + + +ngx_int_t +ngx_mail_auth_oauthbearer(ngx_mail_session_t *s, ngx_connection_t *c, + ngx_uint_t n) +{ + u_char *p, *last, *prev; + ngx_str_t *arg, oauth; + + arg = s->args.elts; + + if (s->auth_err.len) { + ngx_log_debug0(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth oauthbearer cancel"); + + if (s->args.nelts == 1 + && ngx_strncmp(arg[0].data, (u_char *) "AQ==", 4) == 0) { s->out = s->auth_err; s->quit = s->auth_quit; @@ -782,16 +885,14 @@ ngx_mail_auth_oauth(ngx_mail_session_t * return NGX_OK; } - invalid: - s->quit = s->auth_quit; ngx_str_null(&s->auth_err); return NGX_MAIL_PARSE_INVALID_COMMAND; } - ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0, - "mail auth oauth: \"%V\" type %ui", &arg[n], auth_method); + ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth oauthbearer: \"%V\"", &arg[n]); oauth.data = ngx_pnalloc(c->pool, ngx_base64_decoded_length(arg[n].len)); if (oauth.data == NULL) { @@ -801,19 +902,113 @@ ngx_mail_auth_oauth(ngx_mail_session_t * if (ngx_decode_base64(&oauth, &arg[n]) != NGX_OK) { ngx_log_error(NGX_LOG_INFO, c->log, 0, "client sent invalid base64 encoding in " - "AUTH XOAUTH2/OAUTHBEARER command"); + "AUTH OAUTHBEARER command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + /* + * RFC 7628 + * "n,a=user at example.com,^A...^Aauth=Bearer ^A^A" + */ + + p = oauth.data; + last = p + oauth.len; + + s->login.len = 0; + prev = NULL; + + while (p < last) { + if (*p == ',') { + if (prev + && (size_t) (p - prev) > sizeof("a=") - 1 + && ngx_strncasecmp(prev, (u_char *) "a=", sizeof("a=") - 1) + == 0) + { + s->login.len = p - prev - (sizeof("a=") - 1); + s->login.data = prev + sizeof("a=") - 1; + break; + } + + p++; + prev = p; + continue; + } + + if (*p == '\1') { + break; + } + + p++; + } + + if (s->login.len == 0) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid login in AUTH OAUTHBEARER command"); return NGX_MAIL_PARSE_INVALID_COMMAND; } - s->passwd.len = oauth.len; - s->passwd.data = oauth.data; + s->passwd.len = 0; + prev = NULL; - ngx_str_null(&s->login); + while (p < last) { + if (*p == '\1') { + if (prev + && (size_t) (p - prev) > sizeof("auth=Bearer ") - 1 + && ngx_strncasecmp(prev, (u_char *) "auth=Bearer ", + sizeof("auth=Bearer ") - 1) + == 0) + { + s->passwd.len = p - prev - (sizeof("auth=Bearer ") - 1); + s->passwd.data = prev + sizeof("auth=Bearer ") - 1; + break; + } + + p++; + prev = p; + continue; + } + + p++; + } + + if (s->passwd.len == 0) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid token in AUTH OAUTHBEARER command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } - ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0, - "mail auth oauth: \"%V\"", &s->passwd); + /* decode =2C =3D in login */ + + p = s->login.data; + last = s->login.data + s->login.len; + + while (p < last) { + if (*p == '=') { + if (p[1] == '2' && (p[2] == 'C' || p[2] == 'c')) { + *p = ','; + + } else if (p[1] == '3' && (p[2] == 'D' || p[2] == 'd')) { + *p = '='; - s->auth_method = auth_method; + } else { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid login in " + "AUTH OAUTHBEARER command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + p += 3; + continue; + } + + p++; + } + + ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth oauthbearer: \"%V\" \"%V\"", + &s->login, &s->passwd); + + s->auth_method = NGX_MAIL_AUTH_OAUTHBEARER; return NGX_DONE; } diff --git a/src/mail/ngx_mail_imap_handler.c b/src/mail/ngx_mail_imap_handler.c --- a/src/mail/ngx_mail_imap_handler.c +++ b/src/mail/ngx_mail_imap_handler.c @@ -222,11 +222,11 @@ ngx_mail_imap_auth_state(ngx_event_t *re break; case ngx_imap_auth_xoauth2: - rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_XOAUTH2); + rc = ngx_mail_auth_xoauth2(s, c, 0); break; case ngx_imap_auth_oauthbearer: - rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_OAUTHBEARER); + rc = ngx_mail_auth_oauthbearer(s, c, 0); break; } @@ -449,7 +449,7 @@ ngx_mail_imap_authenticate(ngx_mail_sess if (s->args.nelts == 2) { s->mail_state = ngx_imap_auth_xoauth2; - return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_XOAUTH2); + return ngx_mail_auth_xoauth2(s, c, 1); } ngx_str_set(&s->out, imap_plain_next); @@ -465,7 +465,7 @@ ngx_mail_imap_authenticate(ngx_mail_sess if (s->args.nelts == 2) { s->mail_state = ngx_imap_auth_oauthbearer; - return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_OAUTHBEARER); + return ngx_mail_auth_oauthbearer(s, c, 1); } ngx_str_set(&s->out, imap_plain_next); diff --git a/src/mail/ngx_mail_pop3_handler.c b/src/mail/ngx_mail_pop3_handler.c --- a/src/mail/ngx_mail_pop3_handler.c +++ b/src/mail/ngx_mail_pop3_handler.c @@ -262,11 +262,11 @@ ngx_mail_pop3_auth_state(ngx_event_t *re break; case ngx_pop3_auth_xoauth2: - rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_XOAUTH2); + rc = ngx_mail_auth_xoauth2(s, c, 0); break; case ngx_pop3_auth_oauthbearer: - rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_OAUTHBEARER); + rc = ngx_mail_auth_oauthbearer(s, c, 0); break; } } @@ -570,7 +570,7 @@ ngx_mail_pop3_auth(ngx_mail_session_t *s if (s->args.nelts == 2) { s->mail_state = ngx_pop3_auth_xoauth2; - return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_XOAUTH2); + return ngx_mail_auth_xoauth2(s, c, 1); } ngx_str_set(&s->out, pop3_next); @@ -586,7 +586,7 @@ ngx_mail_pop3_auth(ngx_mail_session_t *s if (s->args.nelts == 2) { s->mail_state = ngx_pop3_auth_oauthbearer; - return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_OAUTHBEARER); + return ngx_mail_auth_oauthbearer(s, c, 1); } ngx_str_set(&s->out, pop3_next); diff --git a/src/mail/ngx_mail_smtp_handler.c b/src/mail/ngx_mail_smtp_handler.c --- a/src/mail/ngx_mail_smtp_handler.c +++ b/src/mail/ngx_mail_smtp_handler.c @@ -550,11 +550,11 @@ ngx_mail_smtp_auth_state(ngx_event_t *re break; case ngx_smtp_auth_xoauth2: - rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_XOAUTH2); + rc = ngx_mail_auth_xoauth2(s, c, 0); break; case ngx_smtp_auth_oauthbearer: - rc = ngx_mail_auth_oauth(s, c, 0, NGX_MAIL_AUTH_OAUTHBEARER); + rc = ngx_mail_auth_oauthbearer(s, c, 0); break; } } @@ -762,7 +762,7 @@ ngx_mail_smtp_auth(ngx_mail_session_t *s if (s->args.nelts == 2) { s->mail_state = ngx_smtp_auth_xoauth2; - return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_XOAUTH2); + return ngx_mail_auth_xoauth2(s, c, 1); } ngx_str_set(&s->out, smtp_next); @@ -778,7 +778,7 @@ ngx_mail_smtp_auth(ngx_mail_session_t *s if (s->args.nelts == 2) { s->mail_state = ngx_smtp_auth_oauthbearer; - return ngx_mail_auth_oauth(s, c, 1, NGX_MAIL_AUTH_OAUTHBEARER); + return ngx_mail_auth_oauthbearer(s, c, 1); } ngx_str_set(&s->out, smtp_next); Patches for tests: # HG changeset patch # User Maxim Dounin # Date 1716247768 -10800 # Tue May 21 02:29:28 2024 +0300 # Node ID 7c476ab61fc4836df946ae65c2d83f9cf3b2e51e # Parent 79753dd514e60b47375e36c54739ea434e04a5b6 Tests: added test that EXTERNAL mail auth clears old password. diff --git a/mail_imap.t b/mail_imap.t --- a/mail_imap.t +++ b/mail_imap.t @@ -93,7 +93,7 @@ http { EOF $t->run_daemon(\&Test::Nginx::IMAP::imap_test_daemon); -$t->run()->plan(29); +$t->run()->plan(30); $t->waitforsocket('127.0.0.1:' . port(8144)); @@ -184,6 +184,23 @@ my $s = Test::Nginx::IMAP->new(); $s->send('1 AUTHENTICATE EXTERNAL ' . encode_base64('test at example.com', '')); $s->ok('auth external with username'); +# auth external after failed plain + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.27.1'); + +$s = Test::Nginx::IMAP->new(); +$s->read(); + +$s->send('1 AUTHENTICATE PLAIN ' + . encode_base64("\0test\@example.com\0bad", '')); +$s->read(); + +$s->send('1 AUTHENTICATE EXTERNAL ' . encode_base64('test at example.com', '')); +$s->ok('auth external after plain'); + +} + # quoted strings $s = Test::Nginx::IMAP->new(); # HG changeset patch # User Maxim Dounin # Date 1716249442 -10800 # Tue May 21 02:57:22 2024 +0300 # Node ID c88a117c95eec661bbb8d672f77827ce237d6948 # Parent 7c476ab61fc4836df946ae65c2d83f9cf3b2e51e Tests: added tests for OAUTHBEARER and XOAUTH2 auth methods. Based on a patch by Rob Mueller. diff --git a/mail_oauth.t b/mail_oauth.t new file mode 100644 --- /dev/null +++ b/mail_oauth.t @@ -0,0 +1,331 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for mail module, XOAUTH2 and OAUTHBEARER authentication. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use MIME::Base64; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::IMAP; +use Test::Nginx::POP3; +use Test::Nginx::SMTP; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +local $SIG{PIPE} = 'IGNORE'; + +my $t = Test::Nginx->new()->has(qw/mail imap pop3 smtp http map rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +mail { + proxy_pass_error_message on; + proxy_timeout 15s; + timeout 2s; + auth_http http://127.0.0.1:8080/mail/auth; + + server { + listen 127.0.0.1:8143; + protocol imap; + imap_auth plain oauthbearer xoauth2; + } + server { + listen 127.0.0.1:8110; + protocol pop3; + pop3_auth plain oauthbearer xoauth2; + } + server { + listen 127.0.0.1:8025; + protocol smtp; + smtp_auth plain oauthbearer xoauth2; + } +} + +http { + %%TEST_GLOBALS_HTTP%% + + map $http_auth_protocol $proxy_port { + imap %%PORT_8144%%; + pop3 %%PORT_8111%%; + smtp %%PORT_8026%%; + } + + map $http_auth_pass $reply { + ~secretok OK; + default auth-failed; + } + map $http_auth_pass $passw { + ~secretok secret; + default ""; + } + map $http_auth_pass $sasl { + ~saslfail "eyJzY2hlbWVzIjoiQmVhcmVyIiwic3RhdHVzIjoiNDAwIn0="; + default ""; + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location = /mail/auth { + add_header Auth-Status $reply; + add_header Auth-Server 127.0.0.1; + add_header Auth-Port $proxy_port; + add_header Auth-User test at example.com; + add_header Auth-Pass $passw; + add_header Auth-Wait 1; + add_header Auth-Error-SASL $sasl; + return 204; + } + } +} + +EOF + +$t->run_daemon(\&Test::Nginx::IMAP::imap_test_daemon); +$t->run_daemon(\&Test::Nginx::POP3::pop3_test_daemon); +$t->run_daemon(\&Test::Nginx::SMTP::smtp_test_daemon); +$t->run()->plan(47); + +$t->waitforsocket('127.0.0.1:' . port(8144)); +$t->waitforsocket('127.0.0.1:' . port(8111)); +$t->waitforsocket('127.0.0.1:' . port(8026)); + +############################################################################### + +# AUTHBEARER SASL mechanism +# https://datatracker.ietf.org/doc/html/rfc7628 + +# XOAUTH2 SASL mechanism +# https://developers.google.com/gmail/imap/xoauth2-protocol + +my $s; +my $token = encode_base64( + "n,a=test\@example.com,\001auth=Bearer secretok\001\001", ''); +my $token_saslfail = encode_base64( + "n,a=test\@example.com,\001auth=Bearer saslfail\001\001", ''); +my $token_bad = encode_base64( + "n,a=test\@example.com,\001auth=Bearer bad\001\001", ''); + +my $token_xoauth2 = encode_base64( + "user=test\@example.com\001auth=Bearer secretok\001\001", ''); +my $token_xoauth2_saslfail = encode_base64( + "user=test\@example.com\001auth=Bearer saslfail\001\001", ''); +my $token_xoauth2_bad = encode_base64( + "user=test\@example.com\001auth=Bearer bad\001\001", ''); + +# IMAP + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER ' . $token); +$s->ok('imap oauthbearer success'); + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER'); +$s->check(qr/\+ /, 'imap oauthbearer challenge'); +$s->send($token); +$s->ok('imap oauthbearer success after challenge'); + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER ' . $token_bad); +$s->check(qr/^1 NO auth-failed/, 'imap oauthbearer non-sasl error'); + +sleep(3); + +my @ready = $s->can_read(0); +is(scalar @ready, 1, "imap ready for reading"); +ok($s->eof(), "imap session closed"); + +# fail, sasl failure method + +$s = Test::Nginx::IMAP->new(); +$s->read(); +my $start = time; +$s->send('1 AUTHENTICATE OAUTHBEARER ' . $token_saslfail); +$s->check(qr/^\+ eyJz/, 'imap oauthbearer sasl failure'); +my $wait_time = time - $start; +ok($wait_time >= 1, 'imap oauthbearer error delayed'); +$s->send('AQ=='); +$s->check(qr/^1 NO auth-failed/, + 'imap oauthbearer auth failure after dummy response'); + +# fail, sasl failure method, invalid client response + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER ' . $token_saslfail); +$s->check(qr/^\+ eyJz/, 'imap oauthbearer sasl failure'); +$s->send('foo'); +$s->check(qr/^1 BAD /, 'imap oauthbearer invalid command after invalid line'); + +# fail, sasl failure method, multiple attempts, then success + +$s = Test::Nginx::IMAP->new(); +$s->read(); + +$s->send('1 AUTHENTICATE OAUTHBEARER ' . $token_saslfail); +$s->check(qr/^\+ eyJz/, 'imap oauthbearer sasl failure'); +$s->send('AQ=='); +$s->check(qr/^1 NO auth-failed/, + 'imap oauthbearer auth failure after dummy response'); + +$s->send('1 AUTHENTICATE OAUTHBEARER ' . $token_saslfail); +$s->check(qr/^\+ eyJz/, 'imap oauthbearer sasl failure next'); +$s->send('foo'); +$s->check(qr/^1 BAD/, 'imap oauthbearer invalid command after invalid line'); + +$s->send('1 AUTHENTICATE OAUTHBEARER'); +$s->check(qr/\+ /, 'imap oauthbearer challenge after fail'); +$s->send($token); +$s->ok('imap oauthbearer success after fail'); + +# IMAP XOAUTH2 + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE XOAUTH2 ' . $token_xoauth2); +$s->ok('imap xoauth2 success'); + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE XOAUTH2'); +$s->check(qr/^\+ /, 'imap xoauth2 challenge'); +$s->send($token_xoauth2); +$s->ok('imap xoauth2 success after challenge'); + +$s = Test::Nginx::IMAP->new(); +$s->read(); +$s->send('1 AUTHENTICATE XOAUTH2 ' . $token_xoauth2_saslfail); +$s->check(qr/^\+ eyJz/, 'imap xoauth2 with bad token'); +$s->send(''); +$s->check(qr/^1 NO auth-failed/, 'imap xoauth2 auth failure after empty line'); + +$s->send('1 AUTHENTICATE XOAUTH2 ' . $token_xoauth2_saslfail); +$s->check(qr/^\+ eyJz/, 'imap xoauth2 with bad token next'); +$s->send('foo'); +$s->check(qr/^1 BAD/, 'imap xoauth2 invalid command after invalid line'); + +$s->send('1 AUTHENTICATE XOAUTH2 ' . $token_xoauth2); +$s->ok('imap xoauth2 success after fail'); + +# POP3 + +$s = Test::Nginx::POP3->new(); +$s->read(); +$s->send('AUTH OAUTHBEARER ' . $token); +$s->ok('pop3 oauthbearer success'); + +$s = Test::Nginx::POP3->new(); +$s->read(); +$s->send('AUTH OAUTHBEARER'); +$s->check(qr/^\+ /, 'pop3 oauthbearer challenge'); +$s->send($token); +$s->ok('pop3 oauthbearer success after challenge'); + +$s = Test::Nginx::POP3->new(); +$s->read(); +$s->send('AUTH OAUTHBEARER ' . $token_saslfail); +$s->check(qr/^\+ eyJz/, 'pop3 oauthbearer sasl failure'); +$s->send('AQ=='); +$s->check(qr/^-ERR /, 'pop3 oauthbearer auth failure after dummy response'); + +$s->send('AUTH OAUTHBEARER ' . $token_saslfail); +$s->check(qr/^\+ eyJz/, 'pop3 oauthbearer sasl failure next'); +$s->send(''); +$s->check(qr/^-ERR /, 'pop3 oauthbearer invalid command after invalid line'); + +$s->send('AUTH OAUTHBEARER ' . $token); +$s->ok('pop3 oauthbearer success after fail'); + +# POP3 XOAUTH2 + +$s = Test::Nginx::POP3->new(); +$s->read(); +$s->send('AUTH XOAUTH2 ' . $token_xoauth2); +$s->ok('pop3 xoauth2 success'); + +$s = Test::Nginx::POP3->new(); +$s->read(); +$s->send('AUTH XOAUTH2'); +$s->check(qr/^\+ /, 'pop3 xoauth2 challenge'); +$s->send($token_xoauth2); +$s->ok('pop3 xoauth2 success after challenge'); + +# SMTP + +$s = Test::Nginx::SMTP->new(); +$s->read(); +$s->send('EHLO example.com'); +$s->read(); +$s->send('AUTH OAUTHBEARER ' . $token); +$s->authok('smtp oauthbearer success'); + +$s = Test::Nginx::SMTP->new(); +$s->read(); +$s->send('EHLO example.com'); +$s->read(); +$s->send('AUTH OAUTHBEARER'); +$s->check(qr/^334 /, 'smtp oauthbearer challenge'); +$s->send($token); +$s->authok('smtp oauthbearer success after challenge'); + +$s = Test::Nginx::SMTP->new(); +$s->read(); +$s->send('EHLO example.com'); +$s->read(); +$s->send('AUTH OAUTHBEARER ' . $token_saslfail); +$s->check(qr/^334 eyJz/, 'smtp oauthbearer sasl failure'); +$s->send('AQ=='); +$s->check(qr/^535 /, 'smtp oauthbearer auth failure after dummy response'); + +$s->send('AUTH OAUTHBEARER ' . $token_saslfail); +$s->check(qr/^334 eyJz/, 'smtp oauthbearer sasl failure next'); +$s->send('foo'); +$s->check(qr/^500 /, 'smtp oauthbearer invalid command after invalid line'); + +$s->send('AUTH OAUTHBEARER ' . $token); +$s->authok('smtp oauthbearer success after fail'); + +# SMTP XOAUTH2 + +$s = Test::Nginx::SMTP->new(); +$s->read(); +$s->send('EHLO example.com'); +$s->read(); +$s->send('AUTH XOAUTH2 ' . $token_xoauth2); +$s->authok('smtp xoauth2 success'); + +$s = Test::Nginx::SMTP->new(); +$s->read(); +$s->send('EHLO example.com'); +$s->read(); +$s->send('AUTH XOAUTH2'); +$s->check(qr/^334 /, 'smtp xoauth2 challenge'); +$s->send($token_xoauth2); +$s->authok('smtp xoauth2 success after challenge'); + +############################################################################### -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Tue May 21 09:26:38 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Tue, 21 May 2024 12:26:38 +0300 Subject: [PATCH]HTTP/2 connection not properly closing during graceful shutdown In-Reply-To: References: <0255e7345ffdebf8081563334ed7c931718d2c19.camel@kasei.im> Message-ID: Hello! On Fri, May 17, 2024 at 04:35:57PM +0800, Kasei Wang wrote: > Hello! > > On Fri, May 17, 2024 at 2:06?AM Maxim Dounin wrote: > > > > Hello! > > > > On Wed, May 15, 2024 at 11:46:29AM +0800, Kasei Wang wrote: > > > > > Hello, > > > > > > I found that there is a slight probability of HTTP/2 connections not > > > properly closing during graceful shutdown, leading to worker processes > > > in shutting down state remaining stuck for an extended period. After > > > investigation, the issue appears to stem from the following: > > > > > > 1. worker processes in shutting down state use > > > ngx_close_idle_connections to close all idle connections, including > > > HTTP/2 connections. > > > 2. For HTTP/2 connections, c->idle is set to true in ngx_http_v2_init. > > > According to the explanation in > > > , GOAWAY should be sent to > > > all HTTP/2 connections. > > > 3. There might be a time gap between ngx_event_accept and > > > ngx_http_v2_init. For TLS connections, ngx_http_v2_init will be > > > executed after ALPN received, and for plaintext http2 connections, > > > ngx_http_v2_init will be executed after parsing the http2 preface. If > > > ngx_close_idle_connections is executed between ngx_event_accept and > > > ngx_http_v2_init, there's a possibility that c->idle of some > > > connections is set to true AFTER ngx_close_idle_connections, causing > > > those connections to not enter the GOAWAY process and leading to the > > > aforementioned problem. > > > > > > To verify this, I've written a simple HTTP/2 client. This program will > > > wait 15 seconds after TCP connection establishment before starting to > > > send data. The purpose of sleep is to to raise the probability of > > > encountering the issue. You can reproduce the problem by executing > > > "nginx -s reload" during this 15-second wait. If you're interested, you > > > can try my test program > > > () to reproduce > > > the issue. > > > > > > The following patch would call ngx_http_v2_finalize_connection to close > > > http2 connections which is initialized after > > > ngx_close_idle_connections. > > > > > > And here are some previous discussion on the another maillist: > > > > > > > > > > > > Please confirm if this issue exists, review my analysis and the patch > > > if possible. Thank you very much. > > > > I think your analysis is correct. Thanks for catching this. > > > > > # HG changeset patch > > > # User Kasei Wang > > > # Date 1715744317 -28800 > > > # Wed May 15 11:38:37 2024 +0800 > > > # Node ID 7f18358cc012039f6bb4e502a6f39a7bd50d669b > > > # Parent 4cfd64a8330dd5bacd839796732fd8c49d87e86e > > > HTTP/2: close http2 connections initialized during graceful shutdown. > > > > > > In some rare cases, a HTTP/2 connections can be initialized during a > > > graceful shutdown. Now close such an connection to avoid unexcepted > > > delays in the graceful shutdown. > > > > > > diff -r 4cfd64a8330d -r 7f18358cc012 src/http/v2/ngx_http_v2.c > > > --- a/src/http/v2/ngx_http_v2.c Tue May 14 17:47:44 2024 +0300 > > > +++ b/src/http/v2/ngx_http_v2.c Wed May 15 11:38:37 2024 +0800 > > > @@ -304,6 +304,11 @@ > > > c->idle = 1; > > > ngx_reusable_connection(c, 0); > > > > > > + if (ngx_exiting) { > > > + ngx_http_v2_finalize_connection(h2c, NGX_HTTP_V2_NO_ERROR); > > > + return; > > > + } > > > + > > > if (c->buffer) { > > > p = c->buffer->pos; > > > end = c->buffer->last; > > > > The patch looks working, though in the case in question the code > > will do various otherwise unneeded things, notably will send > > SETTINGS and WINDOW_UPDATE frames to the client. OTOH, avoiding > > these might be tricky and will require more complex changes, and > > probably don't worth the effort. > > > > Another approach might be to allow just one request if a > > connection is established just before the shutdown, much like > > HTTP/1.x code does. Something as simple as the following patch > > should do the trick: > > > > diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c > > --- a/src/http/v2/ngx_http_v2.c > > +++ b/src/http/v2/ngx_http_v2.c > > @@ -1336,7 +1336,8 @@ ngx_http_v2_state_headers(ngx_http_v2_co > > clcf = ngx_http_get_module_loc_conf(h2c->http_connection->conf_ctx, > > ngx_http_core_module); > > > > - if (clcf->keepalive_timeout == 0 > > + if (ngx_exiting > > + || clcf->keepalive_timeout == 0 > > || h2c->connection->requests >= clcf->keepalive_requests > > || ngx_current_msec - h2c->connection->start_time > > > clcf->keepalive_time) > > > > > > What do you think? [...] > Thanks for your response. > > Actually, I was worried that sending GOAWAY on stream 0 might cause > compatibility issues for some clients. There are so many client > implementations, and I can't be sure that all clients can correctly > retry the request in this situation. > > The disadvantage of your approach is that the worker process may still > need to wait for the first request to arrive. This is not a very long > time, and it should be limited by the client_header_timeout, which is > 60s by default. So I think the waiting is acceptable. The advantage of > this approach is that the process to send GOAWAY frame would be more > similar to the existing reload process, reducing the possibility of > affecting client compatibility. > > Overall, I prefer your approach because it minimizes the change impact > for the client. Thanks for sharing. For completeness, full patch with commit log below. # HG changeset patch # User Maxim Dounin # Date 1716283099 -10800 # Tue May 21 12:18:19 2024 +0300 # Node ID 429d40e8275d2606da7cb710de5bc40a905fe52f # Parent 46ecad404a296042c0088e699f275a92758e5ab9 HTTP/2: handling of connections initialized during shutdown. If an HTTP/2 connection opened before a graceful shutdown, but ngx_http_v2_init() is called after idle connections were closed, such a connection ended up being open till closed by the client (or up to keepalive_time), delaying shutdown. With this change, such connections are allowed to serve just one request, much like it happens in HTTP/1.x, and closed afterwards. Reported by Kasei Wang, https://freenginx.org/pipermail/nginx-devel/2024-May/000277.html diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c --- a/src/http/v2/ngx_http_v2.c +++ b/src/http/v2/ngx_http_v2.c @@ -1336,7 +1336,8 @@ ngx_http_v2_state_headers(ngx_http_v2_co clcf = ngx_http_get_module_loc_conf(h2c->http_connection->conf_ctx, ngx_http_core_module); - if (clcf->keepalive_timeout == 0 + if (ngx_exiting + || clcf->keepalive_timeout == 0 || h2c->connection->requests >= clcf->keepalive_requests || ngx_current_msec - h2c->connection->start_time > clcf->keepalive_time) -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Tue May 21 13:17:26 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Tue, 21 May 2024 16:17:26 +0300 Subject: limiting number of HTTP request headers In-Reply-To: References: Message-ID: Hello! On Thu, May 16, 2024 at 11:01:36AM -0700, Maksim Yevmenkin wrote: > Hello, > > > > Could the community share their thoughts on introducing a directive to > > > cap the number of HTTP request headers? While we currently have the > > > ability to limit client HTTP request buffer size, having more specific > > > control over the number of headers could be advantageous. > > > > I personally tend to think that buffer size limit is enough for > > [free]nginx itself. On the other hand, an additional limit on the > > number of request headers might be beneficial to protect backend > > servers, and might worth adding. > > thanks! a possible patch is attached. > > max > diff --git a/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.c b/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.c > index e26705475e46..b12951fd2a09 100644 > --- a/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.c > +++ b/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.c > @@ -287,6 +287,13 @@ static ngx_command_t ngx_http_core_commands[] = { > offsetof(ngx_http_core_srv_conf_t, underscores_in_headers), > NULL }, > > + { ngx_string("max_request_headers"), > + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, > + ngx_conf_set_num_slot, > + NGX_HTTP_SRV_CONF_OFFSET, > + offsetof(ngx_http_core_srv_conf_t, max_request_headers), > + NULL }, > + Given the names of other related directives, such as: client_header_buffer_size, large_client_header_buffers, client_max_body_size, and so on I tend to think that "max_client_headers" would be a better name. > { ngx_string("location"), > NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_BLOCK|NGX_CONF_TAKE12, > ngx_http_core_location, > @@ -3508,6 +3515,7 @@ ngx_http_core_create_srv_conf(ngx_conf_t *cf) > cscf->ignore_invalid_headers = NGX_CONF_UNSET; > cscf->merge_slashes = NGX_CONF_UNSET; > cscf->underscores_in_headers = NGX_CONF_UNSET; > + cscf->max_request_headers = NGX_CONF_UNSET_UINT; > > cscf->file_name = cf->conf_file->file.name.data; > cscf->line = cf->conf_file->line; > @@ -3554,6 +3562,9 @@ ngx_http_core_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) > ngx_conf_merge_value(conf->underscores_in_headers, > prev->underscores_in_headers, 0); > > + ngx_conf_merge_value(conf->max_request_headers, > + prev->max_request_headers, 128); > + I suspect 128 might have a direct impact on some legitimate workloads. I would rather suggests either NGX_MAX_INT32_VALUE as the default, similarly to max_ranges, effectively switching it off by default, or a larger value, such as 1000, similarly to recently introduced max_commands in the mail proxy module. > if (conf->server_names.nelts == 0) { > /* the array has 4 empty preallocated elements, so push cannot fail */ > sn = ngx_array_push(&conf->server_names); > diff --git a/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.h b/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.h > index 3ed9d1632eec..1a5853fc7dc1 100644 > --- a/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.h > +++ b/ports/netflix/nginx/files/nginx/src/http/ngx_http_core_module.h > @@ -212,6 +212,7 @@ typedef struct { > ngx_flag_t ignore_invalid_headers; > ngx_flag_t merge_slashes; > ngx_flag_t underscores_in_headers; > + ngx_uint_t max_request_headers; /* max number of request headers */ > > unsigned listen:1; > #if (NGX_PCRE) The comment looks unneeded here. > diff --git a/ports/netflix/nginx/files/nginx/src/http/ngx_http_request.c b/ports/netflix/nginx/files/nginx/src/http/ngx_http_request.c > index 2ed0d99c168b..1ff03b215880 100644 > --- a/ports/netflix/nginx/files/nginx/src/http/ngx_http_request.c > +++ b/ports/netflix/nginx/files/nginx/src/http/ngx_http_request.c > @@ -1728,6 +1728,7 @@ ngx_http_process_request_headers(ngx_event_t *rev) > ngx_http_request_t *r; > ngx_http_core_srv_conf_t *cscf; > ngx_http_core_main_conf_t *cmcf; > + ngx_list_part_t *part; Nitpicking: as per style, local variables should be sorted per type length. > > c = rev->data; > r = c->data; > @@ -1816,6 +1817,25 @@ ngx_http_process_request_headers(ngx_event_t *rev) > continue; > } > > + /* apply request header limit */ Note that there are at least two other places where request headers can be added: in HTTP/2 and in HTTP/3. Also, this might be better to add this chunk after the "parsed successfully" comment, right before the ngx_list_push() call. > + > + for (rv = 0, part = &r->headers_in.headers.part; > + part != NULL; > + part = part->next) { > + rv += part->nelts; > + } I don't like the idea of counting all the headers for each header. Introducing a counter might be a better approach. > + > + if (rv >= cscf->max_request_headers) { > + ngx_log_error(NGX_LOG_INFO, c->log, 0, > + "client sent too many request headers, have %i, limit is %ui", > + rv, cscf->max_request_headers); > + > + r->lingering_close = 1; > + ngx_http_finalize_request(r, NGX_HTTP_REQUEST_ENTITY_TOO_LARGE); The 413 error code is not really appropriate here, as it is about the HTTP message body, not headers. In similar situations, such as when client header buffers are exhausted or a header is too long to fit into one buffer, existing code uses internal code 494 (NGX_HTTP_REQUEST_HEADER_TOO_LARGE), which then maps to 400. It might be a good idea to do the same here. (Alternatively, 431 (Request Header Fields Too Large) introduced in RFC 6585 might be appropriate here, yet switching to it will require multiple changes.) > + > + break; > + } > + > /* a header line has been parsed successfully */ > > h = ngx_list_push(&r->headers_in.headers); Below is an attempt to address the above comments, please take a look. # HG changeset patch # User Maxim Dounin # Date 1716296209 -10800 # Tue May 21 15:56:49 2024 +0300 # Node ID 2850868eb5e8c0dff91f86f75cb36886d54af001 # Parent 429d40e8275d2606da7cb710de5bc40a905fe52f Added max_client_headers directive. The directive limits the number of request headers accepted from clients. While the total amount of headers is believed to be sufficiently limited by the existing buffer size limits (client_header_buffer_size and large_client_header_buffers), the additional limit on the number of headers might be beneficial to better protect backend servers. Requested by Maksim Yevmenkin. diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c --- a/src/http/ngx_http_core_module.c +++ b/src/http/ngx_http_core_module.c @@ -252,6 +252,13 @@ static ngx_command_t ngx_http_core_comm offsetof(ngx_http_core_srv_conf_t, large_client_header_buffers), NULL }, + { ngx_string("max_client_headers"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_HTTP_SRV_CONF_OFFSET, + offsetof(ngx_http_core_srv_conf_t, max_client_headers), + NULL }, + { ngx_string("ignore_invalid_headers"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG, ngx_conf_set_flag_slot, @@ -3463,6 +3470,7 @@ ngx_http_core_create_srv_conf(ngx_conf_t cscf->request_pool_size = NGX_CONF_UNSET_SIZE; cscf->client_header_timeout = NGX_CONF_UNSET_MSEC; cscf->client_header_buffer_size = NGX_CONF_UNSET_SIZE; + cscf->max_client_headers = NGX_CONF_UNSET_UINT; cscf->ignore_invalid_headers = NGX_CONF_UNSET; cscf->merge_slashes = NGX_CONF_UNSET; cscf->underscores_in_headers = NGX_CONF_UNSET; @@ -3504,6 +3512,9 @@ ngx_http_core_merge_srv_conf(ngx_conf_t return NGX_CONF_ERROR; } + ngx_conf_merge_uint_value(conf->max_client_headers, + prev->max_client_headers, 1000); + ngx_conf_merge_value(conf->ignore_invalid_headers, prev->ignore_invalid_headers, 1); diff --git a/src/http/ngx_http_core_module.h b/src/http/ngx_http_core_module.h --- a/src/http/ngx_http_core_module.h +++ b/src/http/ngx_http_core_module.h @@ -198,6 +198,8 @@ typedef struct { ngx_msec_t client_header_timeout; + ngx_uint_t max_client_headers; + ngx_flag_t ignore_invalid_headers; ngx_flag_t merge_slashes; ngx_flag_t underscores_in_headers; diff --git a/src/http/ngx_http_request.c b/src/http/ngx_http_request.c --- a/src/http/ngx_http_request.c +++ b/src/http/ngx_http_request.c @@ -1466,6 +1466,15 @@ ngx_http_process_request_headers(ngx_eve /* a header line has been parsed successfully */ + if (r->headers_in.count++ >= cscf->max_client_headers) { + r->lingering_close = 1; + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent too many header lines"); + ngx_http_finalize_request(r, + NGX_HTTP_REQUEST_HEADER_TOO_LARGE); + break; + } + h = ngx_list_push(&r->headers_in.headers); if (h == NULL) { ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); diff --git a/src/http/ngx_http_request.h b/src/http/ngx_http_request.h --- a/src/http/ngx_http_request.h +++ b/src/http/ngx_http_request.h @@ -182,6 +182,7 @@ typedef struct { typedef struct { ngx_list_t headers; + ngx_uint_t count; ngx_table_elt_t *host; ngx_table_elt_t *connection; diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c --- a/src/http/v2/ngx_http_v2.c +++ b/src/http/v2/ngx_http_v2.c @@ -1815,6 +1815,15 @@ ngx_http_v2_state_process_header(ngx_htt } } else { + cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module); + + if (r->headers_in.count++ >= cscf->max_client_headers) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "client sent too many header lines"); + ngx_http_finalize_request(r, NGX_HTTP_REQUEST_HEADER_TOO_LARGE); + goto error; + } + h = ngx_list_push(&r->headers_in.headers); if (h == NULL) { return ngx_http_v2_connection_error(h2c, diff --git a/src/http/v3/ngx_http_v3_request.c b/src/http/v3/ngx_http_v3_request.c --- a/src/http/v3/ngx_http_v3_request.c +++ b/src/http/v3/ngx_http_v3_request.c @@ -657,6 +657,15 @@ ngx_http_v3_process_header(ngx_http_requ } } else { + cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module); + + if (r->headers_in.count++ >= cscf->max_client_headers) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "client sent too many header lines"); + ngx_http_finalize_request(r, NGX_HTTP_REQUEST_HEADER_TOO_LARGE); + return NGX_ERROR; + } + h = ngx_list_push(&r->headers_in.headers); if (h == NULL) { ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); -- Maxim Dounin http://mdounin.ru/ From maksim.yevmenkin at gmail.com Tue May 21 17:37:01 2024 From: maksim.yevmenkin at gmail.com (Maksim Yevmenkin) Date: Tue, 21 May 2024 10:37:01 -0700 Subject: limiting number of HTTP request headers In-Reply-To: References: Message-ID: hello! [...] > Below is an attempt to address the above comments, please take a > look. > > > # HG changeset patch > # User Maxim Dounin > # Date 1716296209 -10800 > # Tue May 21 15:56:49 2024 +0300 > # Node ID 2850868eb5e8c0dff91f86f75cb36886d54af001 > # Parent 429d40e8275d2606da7cb710de5bc40a905fe52f > Added max_client_headers directive. > > The directive limits the number of request headers accepted from clients. > While the total amount of headers is believed to be sufficiently limited > by the existing buffer size limits (client_header_buffer_size and > large_client_header_buffers), the additional limit on the number of headers > might be beneficial to better protect backend servers. > > Requested by Maksim Yevmenkin. this looks good to me. thank you! max From mdounin at mdounin.ru Wed May 22 11:29:14 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Wed, 22 May 2024 14:29:14 +0300 Subject: [PATCH 1 of 4] Core: added socket protocol In-Reply-To: References: Message-ID: Hello! On Thu, May 16, 2024 at 02:38:53PM +0200, maxime wrote: > # HG changeset patch > # User maxime > # Date 1715588655 -7200 > # Mon May 13 10:24:15 2024 +0200 > # Node ID dcadf0a3d97ff4d677440060290e9cda84c69ac7 > # Parent 3d455e37abf870f79be26c36d6b1d9cad2c4dd16 > Core: added socket protocol. > > This patch updates the creation of listening sockets to use a new field > of the `ngx_listening_s` structure. The `protocol` field can be used in > conjunction with the `type` to specify the protocol to be used. > > Modules will then be able to specify a different protocol, e.g. > IPPROTO_MPTCP. > > diff --git a/src/core/ngx_connection.c b/src/core/ngx_connection.c > --- a/src/core/ngx_connection.c > +++ b/src/core/ngx_connection.c > @@ -487,7 +487,18 @@ ngx_open_listening_sockets(ngx_cycle_t * > continue; > } > > - s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); > + s = (ngx_socket_t) -1; > + if (ls[i].protocol > 0) { > + s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, > + ls[i].protocol); > + /* In case of error, retry with the default protocol */ > + ngx_log_error(NGX_LOG_NOTICE, log, 0, > + "socket(%d) failed, trying with 0", ls[i].protocol); > + } > + > + if (s == (ngx_socket_t) -1) { > + s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); > + } > > if (s == (ngx_socket_t) -1) { > ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno, [...] Thanks for the patches. Some comments, in no particular order, and not limited to the particular patch: - Retrying with the default protocol seems to be specific for Multipath TCP, which isn't really a separate protocol, but rather a TCP extension. And should be limited only to Multipath TCP, if done at all. I'm not sure if it needs to be here at all though: as long as Multipath TCP is explicitly requested in the configuration, it might not be a good idea to continue if we aren't able to create such socket. - Generic non-default protocol support will need protocol-specific matching in various places, as well as retrieving the protocol from inherited sockets. E.g., SCTP sockets can coexist with TCP sockets listening on the same address/port pair. - I certainly do not like the idea of #ifndef IPPROTO_MPTCP #define IPPROTO_MPTCP 262 #endif Especially when it is done in each module which creates listen sockets. Similarly, I don't really like the idea of making this Linux-specific. Rather, #ifdef IPPROTO_MPTCP might be a good way to restrict things to systems which do support creation of Multipath TCP listening sockets via socket(IPPROTO_MPTCP). It looks like IPPROTO_MPTCP define is readily available on modern Linux systems, and can be safely used. For example, it is present on Ubuntu 22.04 LTS and later version, so the only supported Ubuntu version where it is not available is 20.04. It is also available on both Rocky Linux 8 and Rocky Linux 9, all supported Alpine Linux versions (3.16 and up), Debian 11 and up (not available only in Debian 10). - I tend to think that "multipath" might be a better name for the "listen" directive parameter. Overall, I rather see it as a "listen ... multipath" at the configuration level, and a listening socket flag "multipath" which is then handled by ngx_open_listening_sockets() as appropriate for a particular platform (currently via socket(IPPROTO_MPTCP) on Linux). Hope this helps. -- Maxim Dounin http://mdounin.ru/ From mux99 at live.be Wed May 22 15:03:22 2024 From: mux99 at live.be (Maxime Dourov) Date: Wed, 22 May 2024 15:03:22 +0000 Subject: [PATCH 1 of 4] Core: added socket protocol In-Reply-To: References: Message-ID: Thank you for your reply! Your comments make sense, I will look at sending a new version later. But this might take a bit more time as I've important deadlines coming soon. ________________________________________ De :?Maxim Dounin Envoy? :?mercredi 22 mai 2024 13:29 ? :?nginx-devel at freenginx.org Cc?:?mux99 at live.be Objet :?Re: [PATCH 1 of 4] Core: added socket protocol ? Hello! On Thu, May 16, 2024 at 02:38:53PM +0200, maxime wrote: > # HG changeset patch > # User maxime > # Date 1715588655 -7200 > #????? Mon May 13 10:24:15 2024 +0200 > # Node ID dcadf0a3d97ff4d677440060290e9cda84c69ac7 > # Parent? 3d455e37abf870f79be26c36d6b1d9cad2c4dd16 > Core: added socket protocol. > > This patch updates the creation of listening sockets to use a new field > of the `ngx_listening_s` structure. The `protocol` field can be used in > conjunction with the `type` to specify the protocol to be used. > > Modules will then be able to specify a different protocol, e.g. > IPPROTO_MPTCP. > > diff --git a/src/core/ngx_connection.c b/src/core/ngx_connection.c > --- a/src/core/ngx_connection.c > +++ b/src/core/ngx_connection.c > @@ -487,7 +487,18 @@ ngx_open_listening_sockets(ngx_cycle_t * >????????????????? continue; >????????????? } >? > -??????????? s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); > +??????????? s = (ngx_socket_t) -1; > +??????????? if (ls[i].protocol > 0) { > +??????????????? s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, > +?????????????????????????????? ls[i].protocol); > +??????????????? /* In case of error, retry with the default protocol */ > +??????????????? ngx_log_error(NGX_LOG_NOTICE, log, 0, > +????????????????????? "socket(%d) failed, trying with 0", ls[i].protocol); > +??????????? } > + > +??????????? if (s == (ngx_socket_t) -1) { > +??????????????? s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); > +??????????? } >? >????????????? if (s == (ngx_socket_t) -1) { >????????????????? ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno, [...] Thanks for the patches. Some comments, in no particular order, and not limited to the particular patch: - Retrying with the default protocol seems to be specific for ? Multipath TCP, which isn't really a separate protocol, but ? rather a TCP extension.? And should be limited only to ? Multipath TCP, if done at all. ? I'm not sure if it needs to be here at all though: as long as ? Multipath TCP is explicitly requested in the configuration, it ? might not be a good idea to continue if we aren't able to create ? such socket. - Generic non-default protocol support will need protocol-specific ? matching in various places, as well as retrieving the protocol ? from inherited sockets.? E.g., SCTP sockets can coexist with ? TCP sockets listening on the same address/port pair. - I certainly do not like the idea of #ifndef IPPROTO_MPTCP #define IPPROTO_MPTCP 262 #endif ? Especially when it is done in each module which creates listen ? sockets.? Similarly, I don't really like the idea of making this ? Linux-specific. ? Rather, #ifdef IPPROTO_MPTCP might be a good way to restrict ? things to systems which do support creation of Multipath TCP ? listening sockets via socket(IPPROTO_MPTCP). ? It looks like IPPROTO_MPTCP define is readily available on ? modern Linux systems, and can be safely used.? For example, it is ? present on Ubuntu 22.04 LTS and later version, so the only ? supported Ubuntu version where it is not available is 20.04.? It ? is also available on both Rocky Linux 8 and Rocky Linux 9, all ? supported Alpine Linux versions (3.16 and up), Debian 11 and up ? (not available only in Debian 10). - I tend to think that "multipath" might be a better name for the ? "listen" directive parameter. Overall, I rather see it as a "listen ... multipath" at the configuration level, and a listening socket flag "multipath" which is then handled by ngx_open_listening_sockets() as appropriate for a particular platform (currently via socket(IPPROTO_MPTCP) on Linux). Hope this helps. -- Maxim Dounin http://mdounin.ru/ ________________________________________ De :?Maxim Dounin Envoy? :?mercredi 22 mai 2024 13:29 ? :?nginx-devel at freenginx.org Cc?:?mux99 at live.be Objet :?Re: [PATCH 1 of 4] Core: added socket protocol ? Hello! On Thu, May 16, 2024 at 02:38:53PM +0200, maxime wrote: > # HG changeset patch > # User maxime > # Date 1715588655 -7200 > #????? Mon May 13 10:24:15 2024 +0200 > # Node ID dcadf0a3d97ff4d677440060290e9cda84c69ac7 > # Parent? 3d455e37abf870f79be26c36d6b1d9cad2c4dd16 > Core: added socket protocol. > > This patch updates the creation of listening sockets to use a new field > of the `ngx_listening_s` structure. The `protocol` field can be used in > conjunction with the `type` to specify the protocol to be used. > > Modules will then be able to specify a different protocol, e.g. > IPPROTO_MPTCP. > > diff --git a/src/core/ngx_connection.c b/src/core/ngx_connection.c > --- a/src/core/ngx_connection.c > +++ b/src/core/ngx_connection.c > @@ -487,7 +487,18 @@ ngx_open_listening_sockets(ngx_cycle_t * >????????????????? continue; >????????????? } >? > -??????????? s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); > +??????????? s = (ngx_socket_t) -1; > +??????????? if (ls[i].protocol > 0) { > +??????????????? s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, > +?????????????????????????????? ls[i].protocol); > +??????????????? /* In case of error, retry with the default protocol */ > +??????????????? ngx_log_error(NGX_LOG_NOTICE, log, 0, > +????????????????????? "socket(%d) failed, trying with 0", ls[i].protocol); > +??????????? } > + > +??????????? if (s == (ngx_socket_t) -1) { > +??????????????? s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); > +??????????? } >? >????????????? if (s == (ngx_socket_t) -1) { >????????????????? ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno, [...] Thanks for the patches. Some comments, in no particular order, and not limited to the particular patch: - Retrying with the default protocol seems to be specific for ? Multipath TCP, which isn't really a separate protocol, but ? rather a TCP extension.? And should be limited only to ? Multipath TCP, if done at all. ? I'm not sure if it needs to be here at all though: as long as ? Multipath TCP is explicitly requested in the configuration, it ? might not be a good idea to continue if we aren't able to create ? such socket. - Generic non-default protocol support will need protocol-specific ? matching in various places, as well as retrieving the protocol ? from inherited sockets.? E.g., SCTP sockets can coexist with ? TCP sockets listening on the same address/port pair. - I certainly do not like the idea of #ifndef IPPROTO_MPTCP #define IPPROTO_MPTCP 262 #endif ? Especially when it is done in each module which creates listen ? sockets.? Similarly, I don't really like the idea of making this ? Linux-specific. ? Rather, #ifdef IPPROTO_MPTCP might be a good way to restrict ? things to systems which do support creation of Multipath TCP ? listening sockets via socket(IPPROTO_MPTCP). ? It looks like IPPROTO_MPTCP define is readily available on ? modern Linux systems, and can be safely used.? For example, it is ? present on Ubuntu 22.04 LTS and later version, so the only ? supported Ubuntu version where it is not available is 20.04.? It ? is also available on both Rocky Linux 8 and Rocky Linux 9, all ? supported Alpine Linux versions (3.16 and up), Debian 11 and up ? (not available only in Debian 10). - I tend to think that "multipath" might be a better name for the ? "listen" directive parameter. Overall, I rather see it as a "listen ... multipath" at the configuration level, and a listening socket flag "multipath" which is then handled by ngx_open_listening_sockets() as appropriate for a particular platform (currently via socket(IPPROTO_MPTCP) on Linux). Hope this helps. -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Thu May 23 15:41:40 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Thu, 23 May 2024 18:41:40 +0300 Subject: limiting number of HTTP request headers In-Reply-To: References: Message-ID: Hello! On Tue, May 21, 2024 at 10:37:01AM -0700, Maksim Yevmenkin wrote: > hello! > > [...] > > > Below is an attempt to address the above comments, please take a > > look. > > > > > > # HG changeset patch > > # User Maxim Dounin > > # Date 1716296209 -10800 > > # Tue May 21 15:56:49 2024 +0300 > > # Node ID 2850868eb5e8c0dff91f86f75cb36886d54af001 > > # Parent 429d40e8275d2606da7cb710de5bc40a905fe52f > > Added max_client_headers directive. > > > > The directive limits the number of request headers accepted from clients. > > While the total amount of headers is believed to be sufficiently limited > > by the existing buffer size limits (client_header_buffer_size and > > large_client_header_buffers), the additional limit on the number of headers > > might be beneficial to better protect backend servers. > > > > Requested by Maksim Yevmenkin. > > this looks good to me. thank you! Looking more into it while writing tests and docs, I tend to think that just "max_headers" would be enough, similarly to "ignore_invalid_headers" and "underscores_in_headers". Mostly identical patch below (s/max_client_headers/max_headers/), as well as tests and docs. I'm going to commit these shortly unless there are objections. Thanks for prodding this! # HG changeset patch # User Maxim Dounin # Date 1716476544 -10800 # Thu May 23 18:02:24 2024 +0300 # Node ID 98d6497ab365f1ae6b44d7e0ab403a1dcf060124 # Parent 46ecad404a296042c0088e699f275a92758e5ab9 Added max_headers directive. The directive limits the number of request headers accepted from clients. While the total amount of headers is believed to be sufficiently limited by the existing buffer size limits (client_header_buffer_size and large_client_header_buffers), the additional limit on the number of headers might be beneficial to better protect backend servers. Requested by Maksim Yevmenkin. diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c --- a/src/http/ngx_http_core_module.c +++ b/src/http/ngx_http_core_module.c @@ -252,6 +252,13 @@ static ngx_command_t ngx_http_core_comm offsetof(ngx_http_core_srv_conf_t, large_client_header_buffers), NULL }, + { ngx_string("max_headers"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_HTTP_SRV_CONF_OFFSET, + offsetof(ngx_http_core_srv_conf_t, max_headers), + NULL }, + { ngx_string("ignore_invalid_headers"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG, ngx_conf_set_flag_slot, @@ -3463,6 +3470,7 @@ ngx_http_core_create_srv_conf(ngx_conf_t cscf->request_pool_size = NGX_CONF_UNSET_SIZE; cscf->client_header_timeout = NGX_CONF_UNSET_MSEC; cscf->client_header_buffer_size = NGX_CONF_UNSET_SIZE; + cscf->max_headers = NGX_CONF_UNSET_UINT; cscf->ignore_invalid_headers = NGX_CONF_UNSET; cscf->merge_slashes = NGX_CONF_UNSET; cscf->underscores_in_headers = NGX_CONF_UNSET; @@ -3504,6 +3512,8 @@ ngx_http_core_merge_srv_conf(ngx_conf_t return NGX_CONF_ERROR; } + ngx_conf_merge_uint_value(conf->max_headers, prev->max_headers, 1000); + ngx_conf_merge_value(conf->ignore_invalid_headers, prev->ignore_invalid_headers, 1); diff --git a/src/http/ngx_http_core_module.h b/src/http/ngx_http_core_module.h --- a/src/http/ngx_http_core_module.h +++ b/src/http/ngx_http_core_module.h @@ -198,6 +198,8 @@ typedef struct { ngx_msec_t client_header_timeout; + ngx_uint_t max_headers; + ngx_flag_t ignore_invalid_headers; ngx_flag_t merge_slashes; ngx_flag_t underscores_in_headers; diff --git a/src/http/ngx_http_request.c b/src/http/ngx_http_request.c --- a/src/http/ngx_http_request.c +++ b/src/http/ngx_http_request.c @@ -1466,6 +1466,15 @@ ngx_http_process_request_headers(ngx_eve /* a header line has been parsed successfully */ + if (r->headers_in.count++ >= cscf->max_headers) { + r->lingering_close = 1; + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent too many header lines"); + ngx_http_finalize_request(r, + NGX_HTTP_REQUEST_HEADER_TOO_LARGE); + break; + } + h = ngx_list_push(&r->headers_in.headers); if (h == NULL) { ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); diff --git a/src/http/ngx_http_request.h b/src/http/ngx_http_request.h --- a/src/http/ngx_http_request.h +++ b/src/http/ngx_http_request.h @@ -182,6 +182,7 @@ typedef struct { typedef struct { ngx_list_t headers; + ngx_uint_t count; ngx_table_elt_t *host; ngx_table_elt_t *connection; diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c --- a/src/http/v2/ngx_http_v2.c +++ b/src/http/v2/ngx_http_v2.c @@ -1814,6 +1814,15 @@ ngx_http_v2_state_process_header(ngx_htt } } else { + cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module); + + if (r->headers_in.count++ >= cscf->max_headers) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "client sent too many header lines"); + ngx_http_finalize_request(r, NGX_HTTP_REQUEST_HEADER_TOO_LARGE); + goto error; + } + h = ngx_list_push(&r->headers_in.headers); if (h == NULL) { return ngx_http_v2_connection_error(h2c, diff --git a/src/http/v3/ngx_http_v3_request.c b/src/http/v3/ngx_http_v3_request.c --- a/src/http/v3/ngx_http_v3_request.c +++ b/src/http/v3/ngx_http_v3_request.c @@ -657,6 +657,15 @@ ngx_http_v3_process_header(ngx_http_requ } } else { + cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module); + + if (r->headers_in.count++ >= cscf->max_headers) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "client sent too many header lines"); + ngx_http_finalize_request(r, NGX_HTTP_REQUEST_HEADER_TOO_LARGE); + return NGX_ERROR; + } + h = ngx_list_push(&r->headers_in.headers); if (h == NULL) { ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); Tests: # HG changeset patch # User Maxim Dounin # Date 1716478510 -10800 # Thu May 23 18:35:10 2024 +0300 # Node ID 8350c65afbd64bef22dc90689c0c315f82977f0d # Parent 79753dd514e60b47375e36c54739ea434e04a5b6 Tests: max_client_headers test. diff --git a/h2_max_headers.t b/h2_max_headers.t new file mode 100644 --- /dev/null +++ b/h2_max_headers.t @@ -0,0 +1,92 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for max_headers directive, HTTP/2. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +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_v2 rewrite/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080; + server_name localhost; + + http2 on; + max_headers 5; + + location / { + return 204; + } + } +} + +EOF + +$t->try_run('no max_headers')->plan(3); + +############################################################################### + +like(get('/'), qr/ 204/, 'two headers'); +like(get('/', ('Foo: bar') x 3), qr/ 204/, 'five headers'); +like(get('/', ('Foo: bar') x 4), qr/ 400/, 'six headers rejected'); + +############################################################################### + +sub get { + my ($url, @headers) = @_; + + my $s = Test::Nginx::HTTP2->new(); + my $sid = $s->new_stream({ + headers => [ + { name => ':method', value => 'GET' }, + { name => ':scheme', value => 'http' }, + { name => ':path', value => $url }, + { name => ':authority', value => 'localhost' }, + { name => 'foo', value => 'bar', mode => 2 }, + { name => 'foo', value => 'bar', mode => 2 }, + map { + my ($n, $v) = split /:/; + { name => lc $n, value => $v, mode => 2 }; + } @headers + ] + }); + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}); +} + +############################################################################### diff --git a/h3_max_headers.t b/h3_max_headers.t new file mode 100644 --- /dev/null +++ b/h3_max_headers.t @@ -0,0 +1,112 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for max_headers directive, HTTP/3. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP3; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_v3 rewrite cryptx/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + ssl_certificate localhost.crt; + ssl_certificate_key localhost.key; + + server { + listen 127.0.0.1:%%PORT_8980_UDP%% quic; + server_name localhost; + + max_headers 5; + + location / { + return 204; + } + } +} + +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->try_run('no max_headers')->plan(3); + +############################################################################### + +like(get('/'), qr/ 204/, 'two headers'); +like(get('/', ('Foo: bar') x 3), qr/ 204/, 'five headers'); +like(get('/', ('Foo: bar') x 4), qr/ 400/, 'six headers rejected'); + +############################################################################### + +sub get { + my ($url, @headers) = @_; + + my $s = Test::Nginx::HTTP3->new(); + my $sid = $s->new_stream({ + headers => [ + { name => ':method', value => 'GET' }, + { name => ':scheme', value => 'http' }, + { name => ':path', value => $url }, + { name => ':authority', value => 'localhost' }, + { name => 'foo', value => 'bar' }, + { name => 'foo', value => 'bar' }, + map { + my ($n, $v) = split /:/; + { name => lc $n, value => $v }; + } @headers + ] + }); + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}); +} + +############################################################################### diff --git a/http_max_headers.t b/http_max_headers.t new file mode 100644 --- /dev/null +++ b/http_max_headers.t @@ -0,0 +1,73 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for max_headers directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080; + server_name localhost; + + max_headers 5; + + location / { + return 204; + } + } +} + +EOF + +$t->try_run('no max_headers')->plan(3); + +############################################################################### + +like(get('/'), qr/ 204/, 'two headers'); +like(get('/', ('Foo: bar') x 3), qr/ 204/, 'five headers'); +like(get('/', ('Foo: bar') x 4), qr/ 400/, 'six headers rejected'); + +############################################################################### + +sub get { + my ($url, @headers) = @_; + return http( + "GET $url HTTP/1.1" . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + join(CRLF, @headers) . CRLF . CRLF + ); +} + +############################################################################### Docs: # HG changeset patch # User Maxim Dounin # Date 1716478618 -10800 # Thu May 23 18:36:58 2024 +0300 # Node ID 89939549eebd6fdaab76b1d0c55450bbcdc4cc0f # Parent 796fbd13ae3662f808365ad8cf5410b53f7dce68 Documented max_headers directive. diff --git a/xml/en/docs/http/ngx_http_core_module.xml b/xml/en/docs/http/ngx_http_core_module.xml --- a/xml/en/docs/http/ngx_http_core_module.xml +++ b/xml/en/docs/http/ngx_http_core_module.xml @@ -10,7 +10,7 @@ + rev="108">
@@ -1778,6 +1778,31 @@ Enables or disables logging of subreques + +number +1000 +http +server +1.27.1 + + +Limits the maximum allowed number of client request header fields. +If the limit is exceeded, the + +error is returned to the client. + + + +If the directive is specified on the level, +the value from the default server can be used. +Details are provided in the +?Virtual +server selection? section. + + + + + number diff --git a/xml/en/docs/http/server_names.xml b/xml/en/docs/http/server_names.xml --- a/xml/en/docs/http/server_names.xml +++ b/xml/en/docs/http/server_names.xml @@ -8,7 +8,7 @@
@@ -404,6 +404,7 @@ the server configuration chosen by SNI; in case of the , , +, and directives involved in processing request header fields, diff --git a/xml/ru/docs/http/ngx_http_core_module.xml b/xml/ru/docs/http/ngx_http_core_module.xml --- a/xml/ru/docs/http/ngx_http_core_module.xml +++ b/xml/ru/docs/http/ngx_http_core_module.xml @@ -10,7 +10,7 @@ + rev="108">
@@ -1774,6 +1774,30 @@ location = /user { + +????? +1000 +http +server +1.27.1 + + +???????????? ???????????? ?????????? ????? ????? ????????? ??????? ???????. +??? ?????????? ?????????? ??????????? ??????? ???????????? ?????? +. + + + +???? ????????? ??????? ?? ?????? , +?? ????? ?????????????? ???????? ?? ??????? ?? ?????????. +????????? ??. ? ??????? +?????? +???????????? ????????. + + + + + ????? diff --git a/xml/ru/docs/http/server_names.xml b/xml/ru/docs/http/server_names.xml --- a/xml/ru/docs/http/server_names.xml +++ b/xml/ru/docs/http/server_names.xml @@ -8,7 +8,7 @@
@@ -408,7 +408,8 @@ server { ? ?????? ????????????? ???????? , - +, + ? , ??????? ????????? ? ????????? ????? ????????? ???????, -- Maxim Dounin http://mdounin.ru/ From maksim.yevmenkin at gmail.com Thu May 23 18:22:17 2024 From: maksim.yevmenkin at gmail.com (Maksim Yevmenkin) Date: Thu, 23 May 2024 11:22:17 -0700 Subject: limiting number of HTTP request headers In-Reply-To: References: Message-ID: hello! [...] > Looking more into it while writing tests and docs, I tend to think > that just "max_headers" would be enough, similarly to > "ignore_invalid_headers" and "underscores_in_headers". > > Mostly identical patch below (s/max_client_headers/max_headers/), > as well as tests and docs. I'm going to commit these shortly > unless there are objections. > > Thanks for prodding this! no objections, thank you! max From mdounin at mdounin.ru Thu May 23 22:17:27 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 24 May 2024 01:17:27 +0300 Subject: [nginx] Added max_headers directive. Message-ID: details: http://freenginx.org/hg/nginx/rev/199dc0d6b05b branches: changeset: 9275:199dc0d6b05b user: Maxim Dounin date: Fri May 24 00:20:01 2024 +0300 description: Added max_headers directive. The directive limits the number of request headers accepted from clients. While the total amount of headers is believed to be sufficiently limited by the existing buffer size limits (client_header_buffer_size and large_client_header_buffers), the additional limit on the number of headers might be beneficial to better protect backend servers. Requested by Maksim Yevmenkin. diffstat: src/http/ngx_http_core_module.c | 10 ++++++++++ src/http/ngx_http_core_module.h | 2 ++ src/http/ngx_http_request.c | 9 +++++++++ src/http/ngx_http_request.h | 1 + src/http/v2/ngx_http_v2.c | 9 +++++++++ src/http/v3/ngx_http_v3_request.c | 9 +++++++++ 6 files changed, 40 insertions(+), 0 deletions(-) diffs (114 lines): diff --git a/src/http/ngx_http_core_module.c b/src/http/ngx_http_core_module.c --- a/src/http/ngx_http_core_module.c +++ b/src/http/ngx_http_core_module.c @@ -252,6 +252,13 @@ static ngx_command_t ngx_http_core_comm offsetof(ngx_http_core_srv_conf_t, large_client_header_buffers), NULL }, + { ngx_string("max_headers"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_HTTP_SRV_CONF_OFFSET, + offsetof(ngx_http_core_srv_conf_t, max_headers), + NULL }, + { ngx_string("ignore_invalid_headers"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_CONF_FLAG, ngx_conf_set_flag_slot, @@ -3463,6 +3470,7 @@ ngx_http_core_create_srv_conf(ngx_conf_t cscf->request_pool_size = NGX_CONF_UNSET_SIZE; cscf->client_header_timeout = NGX_CONF_UNSET_MSEC; cscf->client_header_buffer_size = NGX_CONF_UNSET_SIZE; + cscf->max_headers = NGX_CONF_UNSET_UINT; cscf->ignore_invalid_headers = NGX_CONF_UNSET; cscf->merge_slashes = NGX_CONF_UNSET; cscf->underscores_in_headers = NGX_CONF_UNSET; @@ -3504,6 +3512,8 @@ ngx_http_core_merge_srv_conf(ngx_conf_t return NGX_CONF_ERROR; } + ngx_conf_merge_uint_value(conf->max_headers, prev->max_headers, 1000); + ngx_conf_merge_value(conf->ignore_invalid_headers, prev->ignore_invalid_headers, 1); diff --git a/src/http/ngx_http_core_module.h b/src/http/ngx_http_core_module.h --- a/src/http/ngx_http_core_module.h +++ b/src/http/ngx_http_core_module.h @@ -198,6 +198,8 @@ typedef struct { ngx_msec_t client_header_timeout; + ngx_uint_t max_headers; + ngx_flag_t ignore_invalid_headers; ngx_flag_t merge_slashes; ngx_flag_t underscores_in_headers; diff --git a/src/http/ngx_http_request.c b/src/http/ngx_http_request.c --- a/src/http/ngx_http_request.c +++ b/src/http/ngx_http_request.c @@ -1466,6 +1466,15 @@ ngx_http_process_request_headers(ngx_eve /* a header line has been parsed successfully */ + if (r->headers_in.count++ >= cscf->max_headers) { + r->lingering_close = 1; + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent too many header lines"); + ngx_http_finalize_request(r, + NGX_HTTP_REQUEST_HEADER_TOO_LARGE); + break; + } + h = ngx_list_push(&r->headers_in.headers); if (h == NULL) { ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); diff --git a/src/http/ngx_http_request.h b/src/http/ngx_http_request.h --- a/src/http/ngx_http_request.h +++ b/src/http/ngx_http_request.h @@ -182,6 +182,7 @@ typedef struct { typedef struct { ngx_list_t headers; + ngx_uint_t count; ngx_table_elt_t *host; ngx_table_elt_t *connection; diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c --- a/src/http/v2/ngx_http_v2.c +++ b/src/http/v2/ngx_http_v2.c @@ -1814,6 +1814,15 @@ ngx_http_v2_state_process_header(ngx_htt } } else { + cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module); + + if (r->headers_in.count++ >= cscf->max_headers) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "client sent too many header lines"); + ngx_http_finalize_request(r, NGX_HTTP_REQUEST_HEADER_TOO_LARGE); + goto error; + } + h = ngx_list_push(&r->headers_in.headers); if (h == NULL) { return ngx_http_v2_connection_error(h2c, diff --git a/src/http/v3/ngx_http_v3_request.c b/src/http/v3/ngx_http_v3_request.c --- a/src/http/v3/ngx_http_v3_request.c +++ b/src/http/v3/ngx_http_v3_request.c @@ -657,6 +657,15 @@ ngx_http_v3_process_header(ngx_http_requ } } else { + cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module); + + if (r->headers_in.count++ >= cscf->max_headers) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "client sent too many header lines"); + ngx_http_finalize_request(r, NGX_HTTP_REQUEST_HEADER_TOO_LARGE); + return NGX_ERROR; + } + h = ngx_list_push(&r->headers_in.headers); if (h == NULL) { ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); From mdounin at mdounin.ru Thu May 23 22:17:44 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 24 May 2024 01:17:44 +0300 Subject: [nginx-tests] Tests: max_client_headers test. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/2d58bb10ff5d branches: changeset: 1979:2d58bb10ff5d user: Maxim Dounin date: Fri May 24 00:36:12 2024 +0300 description: Tests: max_client_headers test. diffstat: h2_max_headers.t | 92 +++++++++++++++++++++++++++++++++++++++++++ h3_max_headers.t | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++ http_max_headers.t | 73 ++++++++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+), 0 deletions(-) diffs (292 lines): diff --git a/h2_max_headers.t b/h2_max_headers.t new file mode 100644 --- /dev/null +++ b/h2_max_headers.t @@ -0,0 +1,92 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for max_headers directive, HTTP/2. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +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_v2 rewrite/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080; + server_name localhost; + + http2 on; + max_headers 5; + + location / { + return 204; + } + } +} + +EOF + +$t->try_run('no max_headers')->plan(3); + +############################################################################### + +like(get('/'), qr/ 204/, 'two headers'); +like(get('/', ('Foo: bar') x 3), qr/ 204/, 'five headers'); +like(get('/', ('Foo: bar') x 4), qr/ 400/, 'six headers rejected'); + +############################################################################### + +sub get { + my ($url, @headers) = @_; + + my $s = Test::Nginx::HTTP2->new(); + my $sid = $s->new_stream({ + headers => [ + { name => ':method', value => 'GET' }, + { name => ':scheme', value => 'http' }, + { name => ':path', value => $url }, + { name => ':authority', value => 'localhost' }, + { name => 'foo', value => 'bar', mode => 2 }, + { name => 'foo', value => 'bar', mode => 2 }, + map { + my ($n, $v) = split /:/; + { name => lc $n, value => $v, mode => 2 }; + } @headers + ] + }); + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}); +} + +############################################################################### diff --git a/h3_max_headers.t b/h3_max_headers.t new file mode 100644 --- /dev/null +++ b/h3_max_headers.t @@ -0,0 +1,112 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for max_headers directive, HTTP/3. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP3; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http http_v3 rewrite cryptx/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + ssl_certificate localhost.crt; + ssl_certificate_key localhost.key; + + server { + listen 127.0.0.1:%%PORT_8980_UDP%% quic; + server_name localhost; + + max_headers 5; + + location / { + return 204; + } + } +} + +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->try_run('no max_headers')->plan(3); + +############################################################################### + +like(get('/'), qr/ 204/, 'two headers'); +like(get('/', ('Foo: bar') x 3), qr/ 204/, 'five headers'); +like(get('/', ('Foo: bar') x 4), qr/ 400/, 'six headers rejected'); + +############################################################################### + +sub get { + my ($url, @headers) = @_; + + my $s = Test::Nginx::HTTP3->new(); + my $sid = $s->new_stream({ + headers => [ + { name => ':method', value => 'GET' }, + { name => ':scheme', value => 'http' }, + { name => ':path', value => $url }, + { name => ':authority', value => 'localhost' }, + { name => 'foo', value => 'bar' }, + { name => 'foo', value => 'bar' }, + map { + my ($n, $v) = split /:/; + { name => lc $n, value => $v }; + } @headers + ] + }); + + my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + + return join("\n", map { "$_: " . $frame->{headers}->{$_}; } + keys %{$frame->{headers}}); +} + +############################################################################### diff --git a/http_max_headers.t b/http_max_headers.t new file mode 100644 --- /dev/null +++ b/http_max_headers.t @@ -0,0 +1,73 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for max_headers directive. + +############################################################################### + +use warnings; +use strict; + +use Test::More; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + server { + listen 127.0.0.1:8080; + server_name localhost; + + max_headers 5; + + location / { + return 204; + } + } +} + +EOF + +$t->try_run('no max_headers')->plan(3); + +############################################################################### + +like(get('/'), qr/ 204/, 'two headers'); +like(get('/', ('Foo: bar') x 3), qr/ 204/, 'five headers'); +like(get('/', ('Foo: bar') x 4), qr/ 400/, 'six headers rejected'); + +############################################################################### + +sub get { + my ($url, @headers) = @_; + return http( + "GET $url HTTP/1.1" . CRLF . + 'Host: localhost' . CRLF . + 'Connection: close' . CRLF . + join(CRLF, @headers) . CRLF . CRLF + ); +} + +############################################################################### From mdounin at mdounin.ru Thu May 23 22:20:03 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 24 May 2024 01:20:03 +0300 Subject: [nginx-site] Documented max_headers directive. Message-ID: details: http://freenginx.org/hg/nginx-site/rev/3b5594157fab branches: changeset: 3083:3b5594157fab user: Maxim Dounin date: Fri May 24 01:16:29 2024 +0300 description: Documented max_headers directive. diffstat: xml/en/docs/http/ngx_http_core_module.xml | 27 ++++++++++++++++++++++++++- xml/en/docs/http/server_names.xml | 3 ++- xml/ru/docs/http/ngx_http_core_module.xml | 26 +++++++++++++++++++++++++- xml/ru/docs/http/server_names.xml | 5 +++-- 4 files changed, 56 insertions(+), 5 deletions(-) diffs (129 lines): diff --git a/xml/en/docs/http/ngx_http_core_module.xml b/xml/en/docs/http/ngx_http_core_module.xml --- a/xml/en/docs/http/ngx_http_core_module.xml +++ b/xml/en/docs/http/ngx_http_core_module.xml @@ -10,7 +10,7 @@ + rev="108">
@@ -1778,6 +1778,31 @@ Enables or disables logging of subreques + +number +1000 +http +server +1.27.1 + + +Limits the maximum allowed number of client request header fields. +If the limit is exceeded, the + +error is returned to the client. + + + +If the directive is specified on the level, +the value from the default server can be used. +Details are provided in the +?Virtual +server selection? section. + + + + + number diff --git a/xml/en/docs/http/server_names.xml b/xml/en/docs/http/server_names.xml --- a/xml/en/docs/http/server_names.xml +++ b/xml/en/docs/http/server_names.xml @@ -8,7 +8,7 @@
@@ -404,6 +404,7 @@ the server configuration chosen by SNI; in case of the , , +, and directives involved in processing request header fields, diff --git a/xml/ru/docs/http/ngx_http_core_module.xml b/xml/ru/docs/http/ngx_http_core_module.xml --- a/xml/ru/docs/http/ngx_http_core_module.xml +++ b/xml/ru/docs/http/ngx_http_core_module.xml @@ -10,7 +10,7 @@ + rev="108">
@@ -1774,6 +1774,30 @@ location = /user { + +????? +1000 +http +server +1.27.1 + + +???????????? ???????????? ?????????? ????? ????? ????????? ??????? ???????. +??? ?????????? ?????????? ??????????? ??????? ???????????? ?????? +. + + + +???? ????????? ??????? ?? ?????? , +?? ????? ?????????????? ???????? ?? ??????? ?? ?????????. +????????? ??. ? ??????? +?????? +???????????? ????????. + + + + + ????? diff --git a/xml/ru/docs/http/server_names.xml b/xml/ru/docs/http/server_names.xml --- a/xml/ru/docs/http/server_names.xml +++ b/xml/ru/docs/http/server_names.xml @@ -8,7 +8,7 @@
@@ -408,7 +408,8 @@ server { ? ?????? ????????????? ???????? , - +, + ? , ??????? ????????? ? ????????? ????? ????????? ???????, From mdounin at mdounin.ru Fri May 24 00:54:13 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 24 May 2024 03:54:13 +0300 Subject: [nginx] Upstream: $upstream_cache_key variable. Message-ID: details: http://freenginx.org/hg/nginx/rev/5e7588d2d9cc branches: changeset: 9276:5e7588d2d9cc user: Kirill A. Korinsky date: Thu May 23 18:48:32 2024 +0300 description: Upstream: $upstream_cache_key variable. diffstat: src/http/ngx_http_upstream.c | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 49 insertions(+), 0 deletions(-) diffs (73 lines): diff --git a/src/http/ngx_http_upstream.c b/src/http/ngx_http_upstream.c --- a/src/http/ngx_http_upstream.c +++ b/src/http/ngx_http_upstream.c @@ -23,6 +23,8 @@ static ngx_int_t ngx_http_upstream_cache ngx_http_upstream_t *u); static ngx_int_t ngx_http_upstream_cache_status(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_http_upstream_cache_key(ngx_http_request_t *r, + ngx_http_variable_value_t *v, uintptr_t data); static ngx_int_t ngx_http_upstream_cache_last_modified(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data); static ngx_int_t ngx_http_upstream_cache_etag(ngx_http_request_t *r, @@ -414,6 +416,10 @@ static ngx_http_variable_t ngx_http_ups ngx_http_upstream_cache_status, 0, NGX_HTTP_VAR_NOCACHEABLE, 0 }, + { ngx_string("upstream_cache_key"), NULL, + ngx_http_upstream_cache_key, 0, + NGX_HTTP_VAR_NOCACHEABLE, 0 }, + { ngx_string("upstream_cache_last_modified"), NULL, ngx_http_upstream_cache_last_modified, 0, NGX_HTTP_VAR_NOCACHEABLE|NGX_HTTP_VAR_NOHASH, 0 }, @@ -6004,6 +6010,49 @@ ngx_http_upstream_cache_status(ngx_http_ static ngx_int_t +ngx_http_upstream_cache_key(ngx_http_request_t *r, + ngx_http_variable_value_t *v, uintptr_t data) +{ + u_char *p; + size_t len; + ngx_str_t *key; + ngx_uint_t i; + ngx_http_cache_t *c; + + if (r->cache == NULL || r->cache->keys.nelts == 0) { + v->not_found = 1; + return NGX_OK; + } + + c = r->cache; + + len = 0; + key = c->keys.elts; + + for (i = 0; i < c->keys.nelts; i++) { + len += key[i].len; + } + + p = ngx_pnalloc(r->pool, len); + if (p == NULL) { + return NGX_ERROR; + } + + v->len = len; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = p; + + for (i = 0; i < c->keys.nelts; i++) { + p = ngx_cpymem(p, key[i].data, key[i].len); + } + + return NGX_OK; +} + + +static ngx_int_t ngx_http_upstream_cache_last_modified(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { From mdounin at mdounin.ru Fri May 24 15:57:13 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 24 May 2024 18:57:13 +0300 Subject: [PATCH] Removed incorrect NGX_SUPPRESS_WARN usage Message-ID: # HG changeset patch # User Maxim Dounin # Date 1716562380 -10800 # Fri May 24 17:53:00 2024 +0300 # Node ID e2b22024f6a1c102b3a42016f184cd09e2a49067 # Parent 5e7588d2d9cc8be293eae25f27a319721623db81 Removed incorrect NGX_SUPPRESS_WARN usage. The len variable in ngx_http_variable_unknown_header() needs to be initialized to 0, as it is incremented by subsequent code. diff --git a/src/http/ngx_http_variables.c b/src/http/ngx_http_variables.c --- a/src/http/ngx_http_variables.c +++ b/src/http/ngx_http_variables.c @@ -932,9 +932,7 @@ ngx_http_variable_unknown_header(ngx_htt ngx_table_elt_t *header, *h, **ph; ph = &h; -#if (NGX_SUPPRESS_WARN) len = 0; -#endif header = part->elts; From mdounin at mdounin.ru Sat May 25 00:48:25 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sat, 25 May 2024 03:48:25 +0300 Subject: [PATCH] Tests: logging of certificate subjects instead of issuers Message-ID: # HG changeset patch # User Maxim Dounin # Date 1716597895 -10800 # Sat May 25 03:44:55 2024 +0300 # Node ID d732a96e05dfcad72b03eb82170e9bd08b2e6aae # Parent 2d58bb10ff5d78c07563af5d120a881c6e106c99 Tests: logging of certificate subjects instead of issuers. diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -881,7 +881,7 @@ sub http_start($;%) { or die $IO::Socket::SSL::SSL_ERROR . "\n"; log_in("ssl cipher: " . $s->get_cipher()); - log_in("ssl cert: " . $s->peer_certificate('issuer')); + log_in("ssl cert: " . $s->peer_certificate('subject')); } log_out($request); diff --git a/lib/Test/Nginx/IMAP.pm b/lib/Test/Nginx/IMAP.pm --- a/lib/Test/Nginx/IMAP.pm +++ b/lib/Test/Nginx/IMAP.pm @@ -47,7 +47,7 @@ sub new { my $s = $self->{_socket}; log_in("ssl cipher: " . $s->get_cipher()); - log_in("ssl cert: " . $s->peer_certificate('issuer')); + log_in("ssl cert: " . $s->peer_certificate('subject')); } alarm(0); diff --git a/lib/Test/Nginx/POP3.pm b/lib/Test/Nginx/POP3.pm --- a/lib/Test/Nginx/POP3.pm +++ b/lib/Test/Nginx/POP3.pm @@ -47,7 +47,7 @@ sub new { my $s = $self->{_socket}; log_in("ssl cipher: " . $s->get_cipher()); - log_in("ssl cert: " . $s->peer_certificate('issuer')); + log_in("ssl cert: " . $s->peer_certificate('subject')); } alarm(0); diff --git a/lib/Test/Nginx/SMTP.pm b/lib/Test/Nginx/SMTP.pm --- a/lib/Test/Nginx/SMTP.pm +++ b/lib/Test/Nginx/SMTP.pm @@ -47,7 +47,7 @@ sub new { my $s = $self->{_socket}; log_in("ssl cipher: " . $s->get_cipher()); - log_in("ssl cert: " . $s->peer_certificate('issuer')); + log_in("ssl cert: " . $s->peer_certificate('subject')); } alarm(0); diff --git a/lib/Test/Nginx/Stream.pm b/lib/Test/Nginx/Stream.pm --- a/lib/Test/Nginx/Stream.pm +++ b/lib/Test/Nginx/Stream.pm @@ -63,7 +63,7 @@ sub new { my $s = $self->{_socket}; log_in("ssl cipher: " . $s->get_cipher()); - log_in("ssl cert: " . $s->peer_certificate('issuer')); + log_in("ssl cert: " . $s->peer_certificate('subject')); } alarm(0); From mdounin at mdounin.ru Sun May 26 23:21:24 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 27 May 2024 02:21:24 +0300 Subject: [nginx-site] Documented $upstream_cache_key. Message-ID: details: http://freenginx.org/hg/nginx-site/rev/0bbf14c9fd66 branches: changeset: 3084:0bbf14c9fd66 user: Maxim Dounin date: Fri May 24 03:49:10 2024 +0300 description: Documented $upstream_cache_key. diffstat: xml/en/docs/http/ngx_http_upstream_module.xml | 8 +++++++- xml/ru/docs/http/ngx_http_upstream_module.xml | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diffs (50 lines): diff --git a/xml/en/docs/http/ngx_http_upstream_module.xml b/xml/en/docs/http/ngx_http_upstream_module.xml --- a/xml/en/docs/http/ngx_http_upstream_module.xml +++ b/xml/en/docs/http/ngx_http_upstream_module.xml @@ -10,7 +10,7 @@ + rev="90">
@@ -574,6 +574,12 @@ are separated by commas and colons like $upstream_addr variable. +$upstream_cache_key + + +the cache key being used (1.27.1). + + $upstream_cache_status diff --git a/xml/ru/docs/http/ngx_http_upstream_module.xml b/xml/ru/docs/http/ngx_http_upstream_module.xml --- a/xml/ru/docs/http/ngx_http_upstream_module.xml +++ b/xml/ru/docs/http/ngx_http_upstream_module.xml @@ -10,7 +10,7 @@ + rev="90">
@@ -581,6 +581,12 @@ server { $upstream_addr. +$upstream_cache_key + + +???????????? ???? ??????????? (1.27.1). + + $upstream_cache_status From mdounin at mdounin.ru Mon May 27 21:26:05 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 28 May 2024 00:26:05 +0300 Subject: [PATCH 1 of 2] Added links to Mercurial repositories on download page Message-ID: <329fb2b5e16b065e64cc.1716845165@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1716845006 -10800 # Tue May 28 00:23:26 2024 +0300 # Node ID 329fb2b5e16b065e64ccc249744869ce4e0a6e27 # Parent 0bbf14c9fd6692c95dcca75bd02e979b2f50f45b Added links to Mercurial repositories on download page. Non-linked repo URLs, as well as "read-only" in the description, are artifacts from the time when SVN was used to maintain sources. While here, also listed nginx-tests repository. diff --git a/xml/en/download.xml b/xml/en/download.xml --- a/xml/en/download.xml +++ b/xml/en/download.xml @@ -8,7 +8,7 @@
+ rev="8">
@@ -28,14 +28,17 @@
-Read-only Mercurial repositories: +Mercurial repositories with source code: -code: http://freenginx.org/hg/nginx +code: http://freenginx.org/hg/nginx -site: http://freenginx.org/hg/nginx-site +site: http://freenginx.org/hg/nginx-site + + +tests: http://freenginx.org/hg/nginx-tests diff --git a/xml/ru/download.xml b/xml/ru/download.xml --- a/xml/ru/download.xml +++ b/xml/ru/download.xml @@ -8,7 +8,7 @@
+ rev="8">
@@ -28,14 +28,17 @@
-??????????? Mercurial, ????????? ?????? ??? ??????: +??????????? Mercurial ? ???????? ?????: -???: http://freenginx.org/hg/nginx +???: http://freenginx.org/hg/nginx -????: http://freenginx.org/hg/nginx-site +????: http://freenginx.org/hg/nginx-site + + +?????: http://freenginx.org/hg/nginx-tests From mdounin at mdounin.ru Mon May 27 21:26:06 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 28 May 2024 00:26:06 +0300 Subject: [PATCH 2 of 2] Added link to Github mirror In-Reply-To: <329fb2b5e16b065e64cc.1716845165@vm-bsd.mdounin.ru> References: <329fb2b5e16b065e64cc.1716845165@vm-bsd.mdounin.ru> Message-ID: <40d3fc1cccdb41ba89df.1716845166@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1716845008 -10800 # Tue May 28 00:23:28 2024 +0300 # Node ID 40d3fc1cccdb41ba89dfe934bdf128b65d7ed924 # Parent 329fb2b5e16b065e64ccc249744869ce4e0a6e27 Added link to Github mirror. diff --git a/xml/en/download.xml b/xml/en/download.xml --- a/xml/en/download.xml +++ b/xml/en/download.xml @@ -8,7 +8,7 @@
+ rev="9">
@@ -44,6 +44,11 @@ tests: http: + +Alternatively, Github mirror +is available. + +
diff --git a/xml/ru/download.xml b/xml/ru/download.xml --- a/xml/ru/download.xml +++ b/xml/ru/download.xml @@ -8,7 +8,7 @@
+ rev="9">
@@ -44,6 +44,11 @@ + +????? ???????? +??????? ?? Github. + +
From kasei at kasei.im Tue May 28 02:16:15 2024 From: kasei at kasei.im (Kasei Wang) Date: Tue, 28 May 2024 10:16:15 +0800 Subject: [PATCH]HTTP/2 connection not properly closing during graceful shutdown In-Reply-To: References: <0255e7345ffdebf8081563334ed7c931718d2c19.camel@kasei.im> Message-ID: Hello. I noticed that this patch has not been merged yet. Are there any other issues? Can I help with anything? On Tue, May 21, 2024 at 5:26?PM Maxim Dounin wrote: > > Hello! > > On Fri, May 17, 2024 at 04:35:57PM +0800, Kasei Wang wrote: > > > Hello! > > > > On Fri, May 17, 2024 at 2:06?AM Maxim Dounin wrote: > > > > > > Hello! > > > > > > On Wed, May 15, 2024 at 11:46:29AM +0800, Kasei Wang wrote: > > > > > > > Hello, > > > > > > > > I found that there is a slight probability of HTTP/2 connections not > > > > properly closing during graceful shutdown, leading to worker processes > > > > in shutting down state remaining stuck for an extended period. After > > > > investigation, the issue appears to stem from the following: > > > > > > > > 1. worker processes in shutting down state use > > > > ngx_close_idle_connections to close all idle connections, including > > > > HTTP/2 connections. > > > > 2. For HTTP/2 connections, c->idle is set to true in ngx_http_v2_init. > > > > According to the explanation in > > > > , GOAWAY should be sent to > > > > all HTTP/2 connections. > > > > 3. There might be a time gap between ngx_event_accept and > > > > ngx_http_v2_init. For TLS connections, ngx_http_v2_init will be > > > > executed after ALPN received, and for plaintext http2 connections, > > > > ngx_http_v2_init will be executed after parsing the http2 preface. If > > > > ngx_close_idle_connections is executed between ngx_event_accept and > > > > ngx_http_v2_init, there's a possibility that c->idle of some > > > > connections is set to true AFTER ngx_close_idle_connections, causing > > > > those connections to not enter the GOAWAY process and leading to the > > > > aforementioned problem. > > > > > > > > To verify this, I've written a simple HTTP/2 client. This program will > > > > wait 15 seconds after TCP connection establishment before starting to > > > > send data. The purpose of sleep is to to raise the probability of > > > > encountering the issue. You can reproduce the problem by executing > > > > "nginx -s reload" during this 15-second wait. If you're interested, you > > > > can try my test program > > > > () to reproduce > > > > the issue. > > > > > > > > The following patch would call ngx_http_v2_finalize_connection to close > > > > http2 connections which is initialized after > > > > ngx_close_idle_connections. > > > > > > > > And here are some previous discussion on the another maillist: > > > > > > > > > > > > > > > > Please confirm if this issue exists, review my analysis and the patch > > > > if possible. Thank you very much. > > > > > > I think your analysis is correct. Thanks for catching this. > > > > > > > # HG changeset patch > > > > # User Kasei Wang > > > > # Date 1715744317 -28800 > > > > # Wed May 15 11:38:37 2024 +0800 > > > > # Node ID 7f18358cc012039f6bb4e502a6f39a7bd50d669b > > > > # Parent 4cfd64a8330dd5bacd839796732fd8c49d87e86e > > > > HTTP/2: close http2 connections initialized during graceful shutdown. > > > > > > > > In some rare cases, a HTTP/2 connections can be initialized during a > > > > graceful shutdown. Now close such an connection to avoid unexcepted > > > > delays in the graceful shutdown. > > > > > > > > diff -r 4cfd64a8330d -r 7f18358cc012 src/http/v2/ngx_http_v2.c > > > > --- a/src/http/v2/ngx_http_v2.c Tue May 14 17:47:44 2024 +0300 > > > > +++ b/src/http/v2/ngx_http_v2.c Wed May 15 11:38:37 2024 +0800 > > > > @@ -304,6 +304,11 @@ > > > > c->idle = 1; > > > > ngx_reusable_connection(c, 0); > > > > > > > > + if (ngx_exiting) { > > > > + ngx_http_v2_finalize_connection(h2c, NGX_HTTP_V2_NO_ERROR); > > > > + return; > > > > + } > > > > + > > > > if (c->buffer) { > > > > p = c->buffer->pos; > > > > end = c->buffer->last; > > > > > > The patch looks working, though in the case in question the code > > > will do various otherwise unneeded things, notably will send > > > SETTINGS and WINDOW_UPDATE frames to the client. OTOH, avoiding > > > these might be tricky and will require more complex changes, and > > > probably don't worth the effort. > > > > > > Another approach might be to allow just one request if a > > > connection is established just before the shutdown, much like > > > HTTP/1.x code does. Something as simple as the following patch > > > should do the trick: > > > > > > diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c > > > --- a/src/http/v2/ngx_http_v2.c > > > +++ b/src/http/v2/ngx_http_v2.c > > > @@ -1336,7 +1336,8 @@ ngx_http_v2_state_headers(ngx_http_v2_co > > > clcf = ngx_http_get_module_loc_conf(h2c->http_connection->conf_ctx, > > > ngx_http_core_module); > > > > > > - if (clcf->keepalive_timeout == 0 > > > + if (ngx_exiting > > > + || clcf->keepalive_timeout == 0 > > > || h2c->connection->requests >= clcf->keepalive_requests > > > || ngx_current_msec - h2c->connection->start_time > > > > clcf->keepalive_time) > > > > > > > > > What do you think? > > [...] > > > Thanks for your response. > > > > Actually, I was worried that sending GOAWAY on stream 0 might cause > > compatibility issues for some clients. There are so many client > > implementations, and I can't be sure that all clients can correctly > > retry the request in this situation. > > > > The disadvantage of your approach is that the worker process may still > > need to wait for the first request to arrive. This is not a very long > > time, and it should be limited by the client_header_timeout, which is > > 60s by default. So I think the waiting is acceptable. The advantage of > > this approach is that the process to send GOAWAY frame would be more > > similar to the existing reload process, reducing the possibility of > > affecting client compatibility. > > > > Overall, I prefer your approach because it minimizes the change impact > > for the client. > > Thanks for sharing. > > For completeness, full patch with commit log below. > > # HG changeset patch > # User Maxim Dounin > # Date 1716283099 -10800 > # Tue May 21 12:18:19 2024 +0300 > # Node ID 429d40e8275d2606da7cb710de5bc40a905fe52f > # Parent 46ecad404a296042c0088e699f275a92758e5ab9 > HTTP/2: handling of connections initialized during shutdown. > > If an HTTP/2 connection opened before a graceful shutdown, but > ngx_http_v2_init() is called after idle connections were closed, such > a connection ended up being open till closed by the client (or up to > keepalive_time), delaying shutdown. > > With this change, such connections are allowed to serve just one request, > much like it happens in HTTP/1.x, and closed afterwards. > > Reported by Kasei Wang, > https://freenginx.org/pipermail/nginx-devel/2024-May/000277.html > > diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c > --- a/src/http/v2/ngx_http_v2.c > +++ b/src/http/v2/ngx_http_v2.c > @@ -1336,7 +1336,8 @@ ngx_http_v2_state_headers(ngx_http_v2_co > clcf = ngx_http_get_module_loc_conf(h2c->http_connection->conf_ctx, > ngx_http_core_module); > > - if (clcf->keepalive_timeout == 0 > + if (ngx_exiting > + || clcf->keepalive_timeout == 0 > || h2c->connection->requests >= clcf->keepalive_requests > || ngx_current_msec - h2c->connection->start_time > > clcf->keepalive_time) > > -- > Maxim Dounin > http://mdounin.ru/ > -- > nginx-devel mailing list > nginx-devel at freenginx.org > https://freenginx.org/mailman/listinfo/nginx-devel From mdounin at mdounin.ru Tue May 28 17:54:46 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 28 May 2024 20:54:46 +0300 Subject: [nginx] HTTP/2: handling of connections initialized during shutd... Message-ID: details: http://freenginx.org/hg/nginx/rev/4a0cd107c0f1 branches: changeset: 9277:4a0cd107c0f1 user: Maxim Dounin date: Tue May 28 20:43:50 2024 +0300 description: HTTP/2: handling of connections initialized during shutdown. If an HTTP/2 connection opened before a graceful shutdown, but ngx_http_v2_init() is called after idle connections were closed, such a connection ended up being open till closed by the client (or up to keepalive_time), delaying shutdown. With this change, such connections are allowed to serve just one request, much like it happens in HTTP/1.x, and closed afterwards. Reported by Kasei Wang, https://freenginx.org/pipermail/nginx-devel/2024-May/000277.html diffstat: src/http/v2/ngx_http_v2.c | 3 ++- 1 files changed, 2 insertions(+), 1 deletions(-) diffs (13 lines): diff --git a/src/http/v2/ngx_http_v2.c b/src/http/v2/ngx_http_v2.c --- a/src/http/v2/ngx_http_v2.c +++ b/src/http/v2/ngx_http_v2.c @@ -1336,7 +1336,8 @@ ngx_http_v2_state_headers(ngx_http_v2_co clcf = ngx_http_get_module_loc_conf(h2c->http_connection->conf_ctx, ngx_http_core_module); - if (clcf->keepalive_timeout == 0 + if (ngx_exiting + || clcf->keepalive_timeout == 0 || h2c->connection->requests >= clcf->keepalive_requests || ngx_current_msec - h2c->connection->start_time > clcf->keepalive_time) From mdounin at mdounin.ru Tue May 28 17:54:56 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Tue, 28 May 2024 20:54:56 +0300 Subject: [PATCH]HTTP/2 connection not properly closing during graceful shutdown In-Reply-To: References: <0255e7345ffdebf8081563334ed7c931718d2c19.camel@kasei.im> Message-ID: Hello! On Tue, May 28, 2024 at 10:16:15AM +0800, Kasei Wang wrote: > I noticed that this patch has not been merged yet. Are there any other > issues? Can I help with anything? I was waiting for a feedback on the final patch version, just in case. Committed, thanks! -- Maxim Dounin http://mdounin.ru/ From maksim.yevmenkin at gmail.com Thu May 30 20:05:08 2024 From: maksim.yevmenkin at gmail.com (Maksim Yevmenkin) Date: Thu, 30 May 2024 13:05:08 -0700 Subject: proxy_cache_lock for content revalidation Message-ID: hello! it seems that the proxy_cache_lock directive operates only for cache misses (new content). while this behavior is documented, i am curious about the reasoning behind it. there are scenarios where proxy_cache_lock could be very beneficial for content revalidation. what are the community's thoughts on this? thanks! max From mdounin at mdounin.ru Thu May 30 23:33:55 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Fri, 31 May 2024 02:33:55 +0300 Subject: proxy_cache_lock for content revalidation In-Reply-To: References: Message-ID: Hello! On Thu, May 30, 2024 at 01:05:08PM -0700, Maksim Yevmenkin wrote: > it seems that the proxy_cache_lock directive operates only for cache > misses (new content). while this behavior is documented, i am curious > about the reasoning behind it. there are scenarios where > proxy_cache_lock could be very beneficial for content revalidation. > what are the community's thoughts on this? The generic idea is that "proxy_cache_use_stale updating;" is a better option for existing cache items. As such, current implementation of proxy_cache_lock doesn't try to handle existing cache items to reduce complexity. Just in case, at least one previous attempt to extend proxy_cache_lock to work with existing cache items can be found here: https://mailman.nginx.org/pipermail/nginx-devel/2018-December/011710.html -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Fri May 31 00:58:25 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 03:58:25 +0300 Subject: [PATCH 0 of 7] QUIC / HTTP/3 fixes merge Message-ID: Hello! The following patch series merges QUIC / HTTP/3 fixes from F5. Additionally, there is a (different) fix for the HTTP/3 connection reuse issue. Comments are welcome. -- Maxim Dounin From mdounin at mdounin.ru Fri May 31 00:58:26 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 03:58:26 +0300 Subject: [PATCH 1 of 7] QUIC: fixed close timer processing with early data In-Reply-To: References: Message-ID: <9e9988d46171cfe26fe0.1717117106@vm-bsd.mdounin.ru> # HG changeset patch # User Vladimir Khomutov # Date 1712731090 -10800 # Wed Apr 10 09:38:10 2024 +0300 # Node ID 9e9988d46171cfe26fe0f9d24a51eb38926adbb8 # Parent 4a0cd107c0f1d56cd455df39c37eb0fffc085760 QUIC: fixed close timer processing with early data. The ngx_quic_run() function uses qc->close timer to limit the handshake duration. Normally it is removed by ngx_quic_do_init_streams() which is called once when we are done with initial SSL processing. The problem happens when the client sends early data and streams are initialized in the ngx_quic_run() -> ngx_quic_handle_datagram() call. The order of set/remove timer calls is now reversed; the close timer is set up and the timer fires when assigned, starting the unexpected connection close process. The fix is to skip setting the timer if streams were initialized during handling of the initial datagram. The idle timer for quic is set anyway, and stream-related timeouts are managed by application layer. diff --git a/src/event/quic/ngx_event_quic.c b/src/event/quic/ngx_event_quic.c --- a/src/event/quic/ngx_event_quic.c +++ b/src/event/quic/ngx_event_quic.c @@ -211,7 +211,10 @@ ngx_quic_run(ngx_connection_t *c, ngx_qu qc = ngx_quic_get_connection(c); ngx_add_timer(c->read, qc->tp.max_idle_timeout); - ngx_add_timer(&qc->close, qc->conf->handshake_timeout); + + if (!qc->streams.initialized) { + ngx_add_timer(&qc->close, qc->conf->handshake_timeout); + } ngx_quic_connstate_dbg(c); From mdounin at mdounin.ru Fri May 31 00:58:27 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 03:58:27 +0300 Subject: [PATCH 2 of 7] QUIC: client transport parameter data length checking In-Reply-To: References: Message-ID: # HG changeset patch # User Sergey Kandaurov # Date 1716902239 -14400 # Tue May 28 17:17:19 2024 +0400 # Node ID f9bd4667574b1e6b36a04eb0769c510235df5488 # Parent 9e9988d46171cfe26fe0f9d24a51eb38926adbb8 QUIC: client transport parameter data length checking. diff --git a/src/event/quic/ngx_event_quic_transport.c b/src/event/quic/ngx_event_quic_transport.c --- a/src/event/quic/ngx_event_quic_transport.c +++ b/src/event/quic/ngx_event_quic_transport.c @@ -1750,6 +1750,14 @@ ngx_quic_parse_transport_params(u_char * return NGX_ERROR; } + if ((size_t) (end - p) < len) { + ngx_log_error(NGX_LOG_INFO, log, 0, + "quic failed to parse" + " transport param id:0x%xL, data length %uL too long", + id, len); + return NGX_ERROR; + } + rc = ngx_quic_parse_transport_param(p, p + len, id, tp); if (rc == NGX_ERROR) { From mdounin at mdounin.ru Fri May 31 00:58:28 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 03:58:28 +0300 Subject: [PATCH 3 of 7] HTTP/3: fixed dynamic table overflow In-Reply-To: References: Message-ID: <352360116e2c2fef891a.1717117108@vm-bsd.mdounin.ru> # HG changeset patch # User Roman Arutyunyan # Date 1716902330 -14400 # Tue May 28 17:18:50 2024 +0400 # Node ID 352360116e2c2fef891a91284083fb1d6c36c72d # Parent f9bd4667574b1e6b36a04eb0769c510235df5488 HTTP/3: fixed dynamic table overflow. While inserting a new entry into the dynamic table, first the entry is added, and then older entries are evicted until table size is within capacity. After the first step, the number of entries may temporarily exceed the maximum calculated from capacity by one entry, which previously caused table overflow. The easiest way to trigger the issue is to keep adding entries with empty names and values until first eviction. The issue was introduced by 987bee4363d1. diff --git a/src/http/v3/ngx_http_v3_table.c b/src/http/v3/ngx_http_v3_table.c --- a/src/http/v3/ngx_http_v3_table.c +++ b/src/http/v3/ngx_http_v3_table.c @@ -308,7 +308,7 @@ ngx_http_v3_set_capacity(ngx_connection_ prev_max = dt->capacity / 32; if (max > prev_max) { - elts = ngx_alloc(max * sizeof(void *), c->log); + elts = ngx_alloc((max + 1) * sizeof(void *), c->log); if (elts == NULL) { return NGX_ERROR; } From mdounin at mdounin.ru Fri May 31 00:58:29 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 03:58:29 +0300 Subject: [PATCH 4 of 7] QUIC: ignore CRYPTO frames after handshake completion In-Reply-To: References: Message-ID: <8822af43e7e919b8c903.1717117109@vm-bsd.mdounin.ru> # HG changeset patch # User Roman Arutyunyan # Date 1716902348 -14400 # Tue May 28 17:19:08 2024 +0400 # Node ID 8822af43e7e919b8c903174886c8ffecdb25f017 # Parent 352360116e2c2fef891a91284083fb1d6c36c72d QUIC: ignore CRYPTO frames after handshake completion. Sending handshake-level CRYPTO frames after the client's Finished message could lead to memory disclosure and a potential segfault, if those frames are sent in one packet with the Finished frame. diff --git a/src/event/quic/ngx_event_quic_ssl.c b/src/event/quic/ngx_event_quic_ssl.c --- a/src/event/quic/ngx_event_quic_ssl.c +++ b/src/event/quic/ngx_event_quic_ssl.c @@ -326,6 +326,11 @@ ngx_quic_handle_crypto_frame(ngx_connect ngx_quic_crypto_frame_t *f; qc = ngx_quic_get_connection(c); + + if (!ngx_quic_keys_available(qc->keys, pkt->level, 0)) { + return NGX_OK; + } + ctx = ngx_quic_get_send_ctx(qc, pkt->level); f = &frame->u.crypto; From mdounin at mdounin.ru Fri May 31 00:58:30 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 03:58:30 +0300 Subject: [PATCH 5 of 7] QUIC: ngx_quic_buffer_t use-after-free protection In-Reply-To: References: Message-ID: <9e69f2b7026deeed2824.1717117110@vm-bsd.mdounin.ru> # HG changeset patch # User Roman Arutyunyan # Date 1716902361 -14400 # Tue May 28 17:19:21 2024 +0400 # Node ID 9e69f2b7026deeed2824422ef64d11e234c05bb1 # Parent 8822af43e7e919b8c903174886c8ffecdb25f017 QUIC: ngx_quic_buffer_t use-after-free protection. Previously the last chain field of ngx_quic_buffer_t could still reference freed chains and buffers after calling ngx_quic_free_buffer(). While normally an ngx_quic_buffer_t object should not be used after freeing, resetting last_chain field would prevent a potential use-after-free. diff --git a/src/event/quic/ngx_event_quic_frames.c b/src/event/quic/ngx_event_quic_frames.c --- a/src/event/quic/ngx_event_quic_frames.c +++ b/src/event/quic/ngx_event_quic_frames.c @@ -648,6 +648,7 @@ ngx_quic_free_buffer(ngx_connection_t *c ngx_quic_free_chain(c, qb->chain); qb->chain = NULL; + qb->last_chain = NULL; } From mdounin at mdounin.ru Fri May 31 00:58:31 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 03:58:31 +0300 Subject: [PATCH 6 of 7] HTTP/3: fixed handling of zero-length literal field line In-Reply-To: References: Message-ID: <8eecd832d2711dca1f7d.1717117111@vm-bsd.mdounin.ru> # HG changeset patch # User Sergey Kandaurov # Date 1716902445 -14400 # Tue May 28 17:20:45 2024 +0400 # Node ID 8eecd832d2711dca1f7d4eff96369cdc3be514f4 # Parent 9e69f2b7026deeed2824422ef64d11e234c05bb1 HTTP/3: fixed handling of zero-length literal field line. Previously, st->value was passed with NULL data pointer to header handlers. diff --git a/src/http/v3/ngx_http_v3_parse.c b/src/http/v3/ngx_http_v3_parse.c --- a/src/http/v3/ngx_http_v3_parse.c +++ b/src/http/v3/ngx_http_v3_parse.c @@ -810,6 +810,7 @@ ngx_http_v3_parse_field_lri(ngx_connecti st->literal.length = st->pint.value; if (st->literal.length == 0) { + st->value.data = (u_char *) ""; goto done; } @@ -932,6 +933,7 @@ ngx_http_v3_parse_field_l(ngx_connection st->literal.length = st->pint.value; if (st->literal.length == 0) { + st->value.data = (u_char *) ""; goto done; } @@ -1072,6 +1074,7 @@ ngx_http_v3_parse_field_lpbi(ngx_connect st->literal.length = st->pint.value; if (st->literal.length == 0) { + st->value.data = (u_char *) ""; goto done; } From mdounin at mdounin.ru Fri May 31 00:58:32 2024 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 03:58:32 +0300 Subject: [PATCH 7 of 7] HTTP/3: protection from recursion during connection reuse In-Reply-To: References: Message-ID: <5710b269079c37a0bc99.1717117112@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1717116498 -10800 # Fri May 31 03:48:18 2024 +0300 # Node ID 5710b269079c37a0bc99fcfc0e156f971fdfe75a # Parent 8eecd832d2711dca1f7d4eff96369cdc3be514f4 HTTP/3: protection from recursion during connection reuse. When draining a connection associated with an HTTP/3 stream, calling ngx_http_v3_send_cancel_stream() might result in an attempt to obtain a connection for the decoder stream. This in turn will trigger draining of the very same connection. Depending on the client settings, this might either lead to stack overflow or will end up in decoder stream creation error and destroying the connection at some point, potentially resulting in use-after-free on stack. Fix is to make sure that connection reuse is disabled in ngx_http_v3_reset_stream(), so the recursion in question won't happen regardless of what called functions do. diff --git a/src/http/v3/ngx_http_v3_request.c b/src/http/v3/ngx_http_v3_request.c --- a/src/http/v3/ngx_http_v3_request.c +++ b/src/http/v3/ngx_http_v3_request.c @@ -401,6 +401,8 @@ ngx_http_v3_reset_stream(ngx_connection_ ngx_http_v3_session_t *h3c; ngx_http_v3_srv_conf_t *h3scf; + ngx_reusable_connection(c, 0); + h3scf = ngx_http_v3_get_module_srv_conf(c, ngx_http_v3_module); h3c = ngx_http_v3_get_session(c); From mdounin at mdounin.ru Fri May 31 02:27:58 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 05:27:58 +0300 Subject: [nginx] Fixed Valgrind complaints about uninitialized values. Message-ID: details: http://freenginx.org/hg/nginx/rev/f53146df9a47 branches: changeset: 9278:f53146df9a47 user: Maxim Dounin date: Fri May 31 04:38:09 2024 +0300 description: Fixed Valgrind complaints about uninitialized values. In ngx_http_source_charset(), name->data was left uninitialized, and only name->len was set. Since it is used in debug logging, this resulted in the following complaints from Valgrind on systems with musl libc: ==42== Conditional jump or move depends on uninitialised value(s) ==42== at 0x12BC66: memcpy (string.h:51) ==42== by 0x12BC66: ngx_sprintf_str (ngx_string.c:586) ==42== by 0x12C03C: ngx_vslprintf (ngx_string.c:255) ==42== by 0x127694: ngx_log_error_core (ngx_log.c:135) ==42== by 0x1B8795: ngx_http_charset_header_filter (ngx_http_charset_filter_module.c:252) Similarly, ngx_http_split_args() returned uninitialized arg->data, which was then copied to r->args, and also used in debug logging: ==42== Conditional jump or move depends on uninitialised value(s) ==42== at 0x12BC10: memcpy (string.h:50) ==42== by 0x12BC10: ngx_sprintf_str (ngx_string.c:586) ==42== by 0x12C03C: ngx_vslprintf (ngx_string.c:255) ==42== by 0x127694: ngx_log_error_core (ngx_log.c:135) ==42== by 0x184EFB: ngx_http_internal_redirect (ngx_http_core_module.c:2526) ==42== by 0x1D8CCC: ngx_http_try_files_handler (ngx_http_try_files_module.c:209) Fix is to initialize data to NULL. Note that, while memcpy(p, NULL, 0) is also formally undefined now, it is used in multiple places in the code, and expected to be allowed in C2y (see WG14 proposals N3177, N3261, "Allow zero length operations on null pointers"). Prodded by Valgrind. diffstat: src/http/modules/ngx_http_charset_filter_module.c | 1 + src/http/ngx_http_parse.c | 1 + 2 files changed, 2 insertions(+), 0 deletions(-) diffs (22 lines): diff --git a/src/http/modules/ngx_http_charset_filter_module.c b/src/http/modules/ngx_http_charset_filter_module.c --- a/src/http/modules/ngx_http_charset_filter_module.c +++ b/src/http/modules/ngx_http_charset_filter_module.c @@ -438,6 +438,7 @@ ngx_http_source_charset(ngx_http_request if (charset == NGX_HTTP_CHARSET_OFF) { name->len = 0; + name->data = NULL; return charset; } diff --git a/src/http/ngx_http_parse.c b/src/http/ngx_http_parse.c --- a/src/http/ngx_http_parse.c +++ b/src/http/ngx_http_parse.c @@ -2146,6 +2146,7 @@ ngx_http_split_args(ngx_http_request_t * } else { args->len = 0; + args->data = NULL; } } From mdounin at mdounin.ru Fri May 31 02:27:58 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 05:27:58 +0300 Subject: [nginx] Removed incorrect NGX_SUPPRESS_WARN usage. Message-ID: details: http://freenginx.org/hg/nginx/rev/2fce021888f9 branches: changeset: 9279:2fce021888f9 user: Maxim Dounin date: Fri May 31 04:38:40 2024 +0300 description: Removed incorrect NGX_SUPPRESS_WARN usage. The len variable in ngx_http_variable_unknown_header() needs to be initialized to 0, as it is incremented by subsequent code. diffstat: src/http/ngx_http_variables.c | 2 -- 1 files changed, 0 insertions(+), 2 deletions(-) diffs (13 lines): diff --git a/src/http/ngx_http_variables.c b/src/http/ngx_http_variables.c --- a/src/http/ngx_http_variables.c +++ b/src/http/ngx_http_variables.c @@ -932,9 +932,7 @@ ngx_http_variable_unknown_header(ngx_htt ngx_table_elt_t *header, *h, **ph; ph = &h; -#if (NGX_SUPPRESS_WARN) len = 0; -#endif header = part->elts; From mdounin at mdounin.ru Fri May 31 02:30:40 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 05:30:40 +0300 Subject: [nginx-site] Added links to Mercurial repositories on download p... Message-ID: details: http://freenginx.org/hg/nginx-site/rev/329fb2b5e16b branches: changeset: 3085:329fb2b5e16b user: Maxim Dounin date: Tue May 28 00:23:26 2024 +0300 description: Added links to Mercurial repositories on download page. Non-linked repo URLs, as well as "read-only" in the description, are artifacts from the time when SVN was used to maintain sources. While here, also listed nginx-tests repository. diffstat: xml/en/download.xml | 11 +++++++---- xml/ru/download.xml | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diffs (66 lines): diff --git a/xml/en/download.xml b/xml/en/download.xml --- a/xml/en/download.xml +++ b/xml/en/download.xml @@ -8,7 +8,7 @@
+ rev="8">
@@ -28,14 +28,17 @@
-Read-only Mercurial repositories: +Mercurial repositories with source code: -code: http://freenginx.org/hg/nginx +code: http://freenginx.org/hg/nginx -site: http://freenginx.org/hg/nginx-site +site: http://freenginx.org/hg/nginx-site + + +tests: http://freenginx.org/hg/nginx-tests diff --git a/xml/ru/download.xml b/xml/ru/download.xml --- a/xml/ru/download.xml +++ b/xml/ru/download.xml @@ -8,7 +8,7 @@
+ rev="8">
@@ -28,14 +28,17 @@
-??????????? Mercurial, ????????? ?????? ??? ??????: +??????????? Mercurial ? ???????? ?????: -???: http://freenginx.org/hg/nginx +???: http://freenginx.org/hg/nginx -????: http://freenginx.org/hg/nginx-site +????: http://freenginx.org/hg/nginx-site + + +?????: http://freenginx.org/hg/nginx-tests From mdounin at mdounin.ru Fri May 31 02:30:40 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 05:30:40 +0300 Subject: [nginx-site] Added link to Github mirror. Message-ID: details: http://freenginx.org/hg/nginx-site/rev/40d3fc1cccdb branches: changeset: 3086:40d3fc1cccdb user: Maxim Dounin date: Tue May 28 00:23:28 2024 +0300 description: Added link to Github mirror. diffstat: xml/en/download.xml | 7 ++++++- xml/ru/download.xml | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diffs (48 lines): diff --git a/xml/en/download.xml b/xml/en/download.xml --- a/xml/en/download.xml +++ b/xml/en/download.xml @@ -8,7 +8,7 @@
+ rev="9">
@@ -44,6 +44,11 @@ tests: http: + +Alternatively, Github mirror +is available. + +
diff --git a/xml/ru/download.xml b/xml/ru/download.xml --- a/xml/ru/download.xml +++ b/xml/ru/download.xml @@ -8,7 +8,7 @@
+ rev="9">
@@ -44,6 +44,11 @@ + +????? ???????? +??????? ?? Github. + +
From mdounin at mdounin.ru Fri May 31 02:53:04 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 05:53:04 +0300 Subject: [nginx-tests] Tests: logging of certificate subjects instead of ... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/d732a96e05df branches: changeset: 1980:d732a96e05df user: Maxim Dounin date: Sat May 25 03:44:55 2024 +0300 description: Tests: logging of certificate subjects instead of issuers. diffstat: lib/Test/Nginx.pm | 2 +- lib/Test/Nginx/IMAP.pm | 2 +- lib/Test/Nginx/POP3.pm | 2 +- lib/Test/Nginx/SMTP.pm | 2 +- lib/Test/Nginx/Stream.pm | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diffs (60 lines): diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -881,7 +881,7 @@ sub http_start($;%) { or die $IO::Socket::SSL::SSL_ERROR . "\n"; log_in("ssl cipher: " . $s->get_cipher()); - log_in("ssl cert: " . $s->peer_certificate('issuer')); + log_in("ssl cert: " . $s->peer_certificate('subject')); } log_out($request); diff --git a/lib/Test/Nginx/IMAP.pm b/lib/Test/Nginx/IMAP.pm --- a/lib/Test/Nginx/IMAP.pm +++ b/lib/Test/Nginx/IMAP.pm @@ -47,7 +47,7 @@ sub new { my $s = $self->{_socket}; log_in("ssl cipher: " . $s->get_cipher()); - log_in("ssl cert: " . $s->peer_certificate('issuer')); + log_in("ssl cert: " . $s->peer_certificate('subject')); } alarm(0); diff --git a/lib/Test/Nginx/POP3.pm b/lib/Test/Nginx/POP3.pm --- a/lib/Test/Nginx/POP3.pm +++ b/lib/Test/Nginx/POP3.pm @@ -47,7 +47,7 @@ sub new { my $s = $self->{_socket}; log_in("ssl cipher: " . $s->get_cipher()); - log_in("ssl cert: " . $s->peer_certificate('issuer')); + log_in("ssl cert: " . $s->peer_certificate('subject')); } alarm(0); diff --git a/lib/Test/Nginx/SMTP.pm b/lib/Test/Nginx/SMTP.pm --- a/lib/Test/Nginx/SMTP.pm +++ b/lib/Test/Nginx/SMTP.pm @@ -47,7 +47,7 @@ sub new { my $s = $self->{_socket}; log_in("ssl cipher: " . $s->get_cipher()); - log_in("ssl cert: " . $s->peer_certificate('issuer')); + log_in("ssl cert: " . $s->peer_certificate('subject')); } alarm(0); diff --git a/lib/Test/Nginx/Stream.pm b/lib/Test/Nginx/Stream.pm --- a/lib/Test/Nginx/Stream.pm +++ b/lib/Test/Nginx/Stream.pm @@ -63,7 +63,7 @@ sub new { my $s = $self->{_socket}; log_in("ssl cipher: " . $s->get_cipher()); - log_in("ssl cert: " . $s->peer_certificate('issuer')); + log_in("ssl cert: " . $s->peer_certificate('subject')); } alarm(0); From mdounin at mdounin.ru Fri May 31 03:25:38 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 06:25:38 +0300 Subject: [nginx-tests] Tests: avoid changing non-localized $TODO. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/0e2b14c75232 branches: changeset: 1981:0e2b14c75232 user: Maxim Dounin date: Fri May 31 06:22:48 2024 +0300 description: Tests: avoid changing non-localized $TODO. This ensures that there will be no unrelated effects if the variable is actually changed, such as seen on sanitizer tests in 910:49579dd88e3f (reverted by this change). diffstat: lib/Test/Nginx.pm | 18 ++++++++---------- 1 files changed, 8 insertions(+), 10 deletions(-) diffs (33 lines): diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -69,21 +69,19 @@ sub DESTROY { my @alerts = $self->read_file('error.log') =~ /.+\[alert\].+/gm; - if ($^O eq 'solaris') { - $Test::Nginx::TODO = 'alerts' if @alerts - && ! grep { $_ !~ /phantom event/ } @alerts; - } - if ($^O eq 'MSWin32') { - my $re = qr/CloseHandle|TerminateProcess/; - $Test::Nginx::TODO = 'alerts' if @alerts - && ! grep { $_ !~ $re } @alerts; - } + local $Test::Nginx::TODO = 'alerts' if @alerts + && $^O eq 'solaris' + && ! grep { $_ !~ /phantom event/ } @alerts; + + local $Test::Nginx::TODO = 'alerts' if @alerts + && $^O eq 'MSWin32' + && ! grep { $_ !~ qr/CloseHandle|TerminateProcess/ } + @alerts; Test::More::is(join("\n", @alerts), '', 'no alerts'); } if (Test::More->builder->expected_tests) { - local $Test::Nginx::TODO; my $errors = $self->read_file('error.log'); $errors = join "\n", $errors =~ /.+Sanitizer.+/gm; Test::More::is($errors, '', 'no sanitizer errors'); From mdounin at mdounin.ru Fri May 31 03:25:38 2024 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 31 May 2024 06:25:38 +0300 Subject: [nginx-tests] Tests: explicit Valgrind support. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/fb25cbe9d4ec branches: changeset: 1982:fb25cbe9d4ec user: Maxim Dounin date: Fri May 31 06:23:00 2024 +0300 description: Tests: explicit Valgrind support. Valgrind logging is done to a separate file, as it is not able to follow stderr redirection within nginx or append to a file without corrupting it. Further, Valgrind logging seems to interfere with error suppression in tests, and catches various startup errors and warnings, so the log is additionally filtered. Since startup under Valgrind can be really slow, timeout in waitforfile() was changed to 10 seconds. Prodded by Robert Mueller. diffstat: README | 4 ++++ lib/Test/Nginx.pm | 18 +++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diffs (63 lines): diff --git a/README b/README --- a/README +++ b/README @@ -40,6 +40,10 @@ TEST_NGINX_UNSAFE Run unsafe tests. +TEST_NGINX_VALGRIND + + Run nginx under Valgrind during tests. + TEST_NGINX_GLOBALS Sets additional directives in main context. diff --git a/lib/Test/Nginx.pm b/lib/Test/Nginx.pm --- a/lib/Test/Nginx.pm +++ b/lib/Test/Nginx.pm @@ -87,6 +87,12 @@ sub DESTROY { Test::More::is($errors, '', 'no sanitizer errors'); } + if (Test::More->builder->expected_tests && $ENV{TEST_NGINX_VALGRIND}) { + my $errors = $self->read_file('valgrind.log'); + $errors = join "\n", $errors =~ /^==\d+== .+/gm; + Test::More::is($errors, '', 'no valgrind errors'); + } + if ($ENV{TEST_NGINX_CATLOG}) { system("cat $self->{_testdir}/error.log"); } @@ -365,7 +371,10 @@ sub try_run($$) { sub plan($) { my ($self, $plan) = @_; - Test::More::plan(tests => $plan + 2); + $plan += 2; + $plan += 1 if $ENV{TEST_NGINX_VALGRIND}; + + Test::More::plan(tests => $plan); return $self; } @@ -395,7 +404,10 @@ sub run(;$) { my @globals = $self->{_test_globals} ? () : ('-g', "pid $testdir/nginx.pid; " . "error_log $testdir/error.log debug;"); - exec($NGINX, '-p', "$testdir/", '-c', 'nginx.conf', + my @valgrind = (not $ENV{TEST_NGINX_VALGRIND}) ? + () : ('valgrind', '-q', + "--log-file=$testdir/valgrind.log"); + exec(@valgrind, $NGINX, '-p', "$testdir/", '-c', 'nginx.conf', '-e', 'error.log', @globals) or die "Unable to exec(): $!\n"; } @@ -481,7 +493,7 @@ sub waitforfile($;$) { # wait for file to appear # or specified process to exit - for (1 .. 50) { + for (1 .. 100) { return 1 if -e $file; return 0 if $exited; $exited = waitpid($pid, WNOHANG) != 0 if $pid; From maksim.yevmenkin at gmail.com Fri May 31 18:21:01 2024 From: maksim.yevmenkin at gmail.com (Maksim Yevmenkin) Date: Fri, 31 May 2024 11:21:01 -0700 Subject: proxy_cache_lock for content revalidation In-Reply-To: References: Message-ID: hello, > > it seems that the proxy_cache_lock directive operates only for cache > > misses (new content). while this behavior is documented, i am curious > > about the reasoning behind it. there are scenarios where > > proxy_cache_lock could be very beneficial for content revalidation. > > what are the community's thoughts on this? > > The generic idea is that "proxy_cache_use_stale updating;" is a > better option for existing cache items. As such, current > implementation of proxy_cache_lock doesn't try to handle existing > cache items to reduce complexity. right. there are instances where serving outdated content is not permissible, yet overwhelming the upstream servers with a flood of requests is highly undesirable. this situation occurs quite frequently. > Just in case, at least one previous attempt to extend > proxy_cache_lock to work with existing cache items can be found > here: > > https://mailman.nginx.org/pipermail/nginx-devel/2018-December/011710.html thank you! it seems the original post mentioned this exact issue. it also seems that the patch was removed. i am curious if it would be possible to restore the patch. thanks, max From maksim.yevmenkin at gmail.com Fri May 31 18:25:07 2024 From: maksim.yevmenkin at gmail.com (Maksim Yevmenkin) Date: Fri, 31 May 2024 11:25:07 -0700 Subject: proxy_cache_lock for content revalidation In-Reply-To: References: Message-ID: [..] > > > > https://mailman.nginx.org/pipermail/nginx-devel/2018-December/011710.html > > thank you! it seems the original post mentioned this exact issue. it > also seems that the patch was removed. i am curious if it would be > possible to restore the patch. please never mind, i'm blind, sorry! thanks max From mdounin at mdounin.ru Fri May 31 23:03:21 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Sat, 1 Jun 2024 02:03:21 +0300 Subject: [nginx] Add support for XOAUTH2 and OAUTHBEARER authentication In-Reply-To: References: Message-ID: Hello! On Tue, May 21, 2024 at 04:16:05AM +0300, Maxim Dounin wrote: [...] > Additionally, I tend to think that a better approach to > communicate with the auth server might be to parse SASL initial > responses as sent by clients, and provide parsed information, > username and the Bearer token, in the "Auth-User" and "Auth-Pass" > headers, similarly to what we do for other SASL mechanism, such as > PLAIN and CRAM-MD5. > > This will certainly work for XOAUTH2, since the only information > is the username and the Bearer token. In OAUTHBEARER some > additional information can be provided, but I very much doubt it > is used in practice (and, if needed, full initial response > can be sent separately, in a separate header). And parsing > implementation seems to be easy enough. > > Immediate benefits include early correctness checking (e.g., > "n,user=test at example.com,^A..." as used in tests and in some RFC 7628 > examples is incorrect, it should be "n,a=test at example.com,^A..." > instead) and simpler auth server implementation. > > I've provided proof-of-concept as a separate patch below. > > What do you think about this approach? Just for the record, below are login decoding fixes for the proof-of-concept patch in question, as well as test improvements and full merged patch to support XOAUTH2/OAUTHBEARER with parsing. I'm going to commit this shortly unless there are objections. [...] > # HG changeset patch > # User Maxim Dounin > # Date 1716247681 -10800 > # Tue May 21 02:28:01 2024 +0300 > # Node ID 377c966d4623000c0b9a5b2c0d47f9dd64b4cd9d > # Parent b06a347640e565012aced65e2a694a306ed2db5c > Mail: parsing of XOAUTH2 and OAUTHBEARER. > > For both mechanisms, the "Auth-User" header is set to the client identity > obtained from the initial SASL response sent by the client, and the > "Auth-Pass" header is set the Bearer token itself. > > Additionally, only continuation responses correct for the particular > mechanism are now accepted after errors ("AQ==" for OAUTHBEARER, empty > line for XOAUTH2). > > To be merged with the previous patch. [...] > +ngx_int_t > +ngx_mail_auth_oauthbearer(ngx_mail_session_t *s, ngx_connection_t *c, > + ngx_uint_t n) > +{ > + u_char *p, *last, *prev; > + ngx_str_t *arg, oauth; > + > + arg = s->args.elts; > + > + if (s->auth_err.len) { > + ngx_log_debug0(NGX_LOG_DEBUG_MAIL, c->log, 0, > + "mail auth oauthbearer cancel"); > + > + if (s->args.nelts == 1 > + && ngx_strncmp(arg[0].data, (u_char *) "AQ==", 4) == 0) > { > s->out = s->auth_err; > s->quit = s->auth_quit; > @@ -782,16 +885,14 @@ ngx_mail_auth_oauth(ngx_mail_session_t * > return NGX_OK; > } > > - invalid: > - > s->quit = s->auth_quit; > ngx_str_null(&s->auth_err); > > return NGX_MAIL_PARSE_INVALID_COMMAND; > } > > - ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0, > - "mail auth oauth: \"%V\" type %ui", &arg[n], auth_method); > + ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0, > + "mail auth oauthbearer: \"%V\"", &arg[n]); > > oauth.data = ngx_pnalloc(c->pool, ngx_base64_decoded_length(arg[n].len)); > if (oauth.data == NULL) { > @@ -801,19 +902,113 @@ ngx_mail_auth_oauth(ngx_mail_session_t * > if (ngx_decode_base64(&oauth, &arg[n]) != NGX_OK) { > ngx_log_error(NGX_LOG_INFO, c->log, 0, > "client sent invalid base64 encoding in " > - "AUTH XOAUTH2/OAUTHBEARER command"); > + "AUTH OAUTHBEARER command"); > + return NGX_MAIL_PARSE_INVALID_COMMAND; > + } > + > + /* > + * RFC 7628 > + * "n,a=user at example.com,^A...^Aauth=Bearer ^A^A" > + */ > + > + p = oauth.data; > + last = p + oauth.len; > + > + s->login.len = 0; > + prev = NULL; > + > + while (p < last) { > + if (*p == ',') { > + if (prev > + && (size_t) (p - prev) > sizeof("a=") - 1 > + && ngx_strncasecmp(prev, (u_char *) "a=", sizeof("a=") - 1) > + == 0) > + { > + s->login.len = p - prev - (sizeof("a=") - 1); > + s->login.data = prev + sizeof("a=") - 1; > + break; > + } > + > + p++; > + prev = p; > + continue; > + } > + > + if (*p == '\1') { > + break; > + } > + > + p++; > + } > + > + if (s->login.len == 0) { > + ngx_log_error(NGX_LOG_INFO, c->log, 0, > + "client sent invalid login in AUTH OAUTHBEARER command"); > return NGX_MAIL_PARSE_INVALID_COMMAND; > } > > - s->passwd.len = oauth.len; > - s->passwd.data = oauth.data; > + s->passwd.len = 0; > + prev = NULL; > > - ngx_str_null(&s->login); > + while (p < last) { > + if (*p == '\1') { > + if (prev > + && (size_t) (p - prev) > sizeof("auth=Bearer ") - 1 > + && ngx_strncasecmp(prev, (u_char *) "auth=Bearer ", > + sizeof("auth=Bearer ") - 1) > + == 0) > + { > + s->passwd.len = p - prev - (sizeof("auth=Bearer ") - 1); > + s->passwd.data = prev + sizeof("auth=Bearer ") - 1; > + break; > + } > + > + p++; > + prev = p; > + continue; > + } > + > + p++; > + } > + > + if (s->passwd.len == 0) { > + ngx_log_error(NGX_LOG_INFO, c->log, 0, > + "client sent invalid token in AUTH OAUTHBEARER command"); > + return NGX_MAIL_PARSE_INVALID_COMMAND; > + } > > - ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0, > - "mail auth oauth: \"%V\"", &s->passwd); > + /* decode =2C =3D in login */ > + > + p = s->login.data; > + last = s->login.data + s->login.len; > + > + while (p < last) { > + if (*p == '=') { > + if (p[1] == '2' && (p[2] == 'C' || p[2] == 'c')) { > + *p = ','; > + > + } else if (p[1] == '3' && (p[2] == 'D' || p[2] == 'd')) { > + *p = '='; > > - s->auth_method = auth_method; > + } else { > + ngx_log_error(NGX_LOG_INFO, c->log, 0, > + "client sent invalid login in " > + "AUTH OAUTHBEARER command"); > + return NGX_MAIL_PARSE_INVALID_COMMAND; > + } > + > + p += 3; > + continue; > + } > + > + p++; > + } > + > + ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0, > + "mail auth oauthbearer: \"%V\" \"%V\"", > + &s->login, &s->passwd); > + > + s->auth_method = NGX_MAIL_AUTH_OAUTHBEARER; > > return NGX_DONE; > } Login decoding is wrong here, here is a fix: diff --git a/src/mail/ngx_mail_handler.c b/src/mail/ngx_mail_handler.c --- a/src/mail/ngx_mail_handler.c +++ b/src/mail/ngx_mail_handler.c @@ -865,7 +865,7 @@ ngx_int_t ngx_mail_auth_oauthbearer(ngx_mail_session_t *s, ngx_connection_t *c, ngx_uint_t n) { - u_char *p, *last, *prev; + u_char *p, *d, *last, *prev; ngx_str_t *arg, oauth; arg = s->args.elts; @@ -980,15 +980,22 @@ ngx_mail_auth_oauthbearer(ngx_mail_sessi /* decode =2C =3D in login */ p = s->login.data; + d = s->login.data; last = s->login.data + s->login.len; while (p < last) { if (*p == '=') { + + /* + * login is always followed by other data, + * so p[1] and p[2] can be checked directly + */ + if (p[1] == '2' && (p[2] == 'C' || p[2] == 'c')) { - *p = ','; + *d++ = ','; } else if (p[1] == '3' && (p[2] == 'D' || p[2] == 'd')) { - *p = '='; + *d++ = '='; } else { ngx_log_error(NGX_LOG_INFO, c->log, 0, @@ -1001,9 +1008,11 @@ ngx_mail_auth_oauthbearer(ngx_mail_sessi continue; } - p++; + *d++ = *p++; } + s->login.len = d - s->login.data; + ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0, "mail auth oauthbearer: \"%V\" \"%V\"", &s->login, &s->passwd); And patch for tests: diff --git a/mail_oauth.t b/mail_oauth.t --- a/mail_oauth.t +++ b/mail_oauth.t @@ -71,17 +71,18 @@ http { smtp %%PORT_8026%%; } - map $http_auth_pass $reply { - ~secretok OK; + map $http_auth_user:$http_auth_pass $reply { + test at example.com:secretok OK; + test=, at example.com:secretok OK; default auth-failed; } + map $http_auth_pass $passw { - ~secretok secret; - default ""; + secretok secret; } + map $http_auth_pass $sasl { - ~saslfail "eyJzY2hlbWVzIjoiQmVhcmVyIiwic3RhdHVzIjoiNDAwIn0="; - default ""; + saslfail "eyJzY2hlbWVzIjoiQmVhcmVyIiwic3RhdHVzIjoiNDAwIn0="; } server { @@ -92,7 +93,6 @@ http { add_header Auth-Status $reply; add_header Auth-Server 127.0.0.1; add_header Auth-Port $proxy_port; - add_header Auth-User test at example.com; add_header Auth-Pass $passw; add_header Auth-Wait 1; add_header Auth-Error-SASL $sasl; @@ -106,7 +106,7 @@ EOF $t->run_daemon(\&Test::Nginx::IMAP::imap_test_daemon); $t->run_daemon(\&Test::Nginx::POP3::pop3_test_daemon); $t->run_daemon(\&Test::Nginx::SMTP::smtp_test_daemon); -$t->try_run('no oauth support')->plan(47); +$t->try_run('no oauth support')->plan(48); $t->waitforsocket('127.0.0.1:' . port(8144)); $t->waitforsocket('127.0.0.1:' . port(8111)); @@ -123,6 +123,8 @@ EOF my $s; my $token = encode_base64( "n,a=test\@example.com,\001auth=Bearer secretok\001\001", ''); +my $token_escaped = encode_base64( + "n,a=test=3D=2C\@example.com,\001auth=Bearer secretok\001\001", ''); my $token_saslfail = encode_base64( "n,a=test\@example.com,\001auth=Bearer saslfail\001\001", ''); my $token_bad = encode_base64( @@ -144,6 +146,11 @@ my $token_xoauth2_bad = encode_base64( $s = Test::Nginx::IMAP->new(); $s->read(); +$s->send('1 AUTHENTICATE OAUTHBEARER ' . $token_escaped); +$s->ok('imap oauthbearer escaped login'); + +$s = Test::Nginx::IMAP->new(); +$s->read(); $s->send('1 AUTHENTICATE OAUTHBEARER'); $s->check(qr/\+ /, 'imap oauthbearer challenge'); $s->send($token); Full XOAUTH2/OAUTHBEARER patch with parsing: # HG changeset patch # User Maxim Dounin # Date 1717195412 -10800 # Sat Jun 01 01:43:32 2024 +0300 # Node ID a87a01815c540faf782e96078ddd303adef2175d # Parent e73875d3d33e07948136a5eec2d313d6ffdfbe72 Mail: added support for XOAUTH2 and OAUTHBEARER authentication. This patch adds support for the OAUTHBEARER SASL mechanism as defined by RFC 7628, as well as pre-RFC XOAUTH2 SASL mechanism. For both mechanisms, the "Auth-User" header is set to the client identity obtained from the initial SASL response sent by the client, and the "Auth-Pass" header is set to the Bearer token itself. The auth server may return the "Auth-Error-SASL" header, which is passed to the client as an additional SASL challenge. It is expected to contain mechanism-specific error details, base64-encoded. After the client responds (with an empty SASL response for XAUTH2, or with "AQ==" dummy response for OAUTHBEARER), the error message from the "Auth-Status" header is sent. Based on a patch by Rob Mueller. diff --git a/src/mail/ngx_mail.h b/src/mail/ngx_mail.h --- a/src/mail/ngx_mail.h +++ b/src/mail/ngx_mail.h @@ -141,7 +141,9 @@ typedef enum { ngx_pop3_auth_login_password, ngx_pop3_auth_plain, ngx_pop3_auth_cram_md5, - ngx_pop3_auth_external + ngx_pop3_auth_external, + ngx_pop3_auth_xoauth2, + ngx_pop3_auth_oauthbearer } ngx_pop3_state_e; @@ -152,6 +154,8 @@ typedef enum { ngx_imap_auth_plain, ngx_imap_auth_cram_md5, ngx_imap_auth_external, + ngx_imap_auth_xoauth2, + ngx_imap_auth_oauthbearer, ngx_imap_login, ngx_imap_user, ngx_imap_passwd @@ -165,6 +169,8 @@ typedef enum { ngx_smtp_auth_plain, ngx_smtp_auth_cram_md5, ngx_smtp_auth_external, + ngx_smtp_auth_xoauth2, + ngx_smtp_auth_oauthbearer, ngx_smtp_helo, ngx_smtp_helo_xclient, ngx_smtp_helo_auth, @@ -212,8 +218,9 @@ typedef struct { unsigned no_sync_literal:1; unsigned starttls:1; unsigned esmtp:1; - unsigned auth_method:3; + unsigned auth_method:4; unsigned auth_wait:1; + unsigned auth_quit:1; ngx_str_t login; ngx_str_t passwd; @@ -229,6 +236,8 @@ typedef struct { ngx_str_t smtp_from; ngx_str_t smtp_to; + ngx_str_t auth_err; + ngx_str_t cmd; ngx_uint_t command; @@ -303,15 +312,19 @@ typedef struct { #define NGX_MAIL_AUTH_APOP 3 #define NGX_MAIL_AUTH_CRAM_MD5 4 #define NGX_MAIL_AUTH_EXTERNAL 5 -#define NGX_MAIL_AUTH_NONE 6 +#define NGX_MAIL_AUTH_XOAUTH2 6 +#define NGX_MAIL_AUTH_OAUTHBEARER 7 +#define NGX_MAIL_AUTH_NONE 8 -#define NGX_MAIL_AUTH_PLAIN_ENABLED 0x0002 -#define NGX_MAIL_AUTH_LOGIN_ENABLED 0x0004 -#define NGX_MAIL_AUTH_APOP_ENABLED 0x0008 -#define NGX_MAIL_AUTH_CRAM_MD5_ENABLED 0x0010 -#define NGX_MAIL_AUTH_EXTERNAL_ENABLED 0x0020 -#define NGX_MAIL_AUTH_NONE_ENABLED 0x0040 +#define NGX_MAIL_AUTH_PLAIN_ENABLED 0x0002 +#define NGX_MAIL_AUTH_LOGIN_ENABLED 0x0004 +#define NGX_MAIL_AUTH_APOP_ENABLED 0x0008 +#define NGX_MAIL_AUTH_CRAM_MD5_ENABLED 0x0010 +#define NGX_MAIL_AUTH_EXTERNAL_ENABLED 0x0020 +#define NGX_MAIL_AUTH_XOAUTH2_ENABLED 0x0040 +#define NGX_MAIL_AUTH_OAUTHBEARER_ENABLED 0x0080 +#define NGX_MAIL_AUTH_NONE_ENABLED 0x0100 #define NGX_MAIL_PARSE_INVALID_COMMAND 20 @@ -399,6 +412,10 @@ ngx_int_t ngx_mail_auth_cram_md5_salt(ng ngx_int_t ngx_mail_auth_cram_md5(ngx_mail_session_t *s, ngx_connection_t *c); ngx_int_t ngx_mail_auth_external(ngx_mail_session_t *s, ngx_connection_t *c, ngx_uint_t n); +ngx_int_t ngx_mail_auth_xoauth2(ngx_mail_session_t *s, ngx_connection_t *c, + ngx_uint_t n); +ngx_int_t ngx_mail_auth_oauthbearer(ngx_mail_session_t *s, ngx_connection_t *c, + ngx_uint_t n); ngx_int_t ngx_mail_auth_parse(ngx_mail_session_t *s, ngx_connection_t *c); void ngx_mail_send(ngx_event_t *wev); diff --git a/src/mail/ngx_mail_auth_http_module.c b/src/mail/ngx_mail_auth_http_module.c --- a/src/mail/ngx_mail_auth_http_module.c +++ b/src/mail/ngx_mail_auth_http_module.c @@ -53,6 +53,7 @@ struct ngx_mail_auth_http_ctx_s { ngx_str_t err; ngx_str_t errmsg; ngx_str_t errcode; + ngx_str_t errsasl; time_t sleep; @@ -67,6 +68,7 @@ static void ngx_mail_auth_http_ignore_st static void ngx_mail_auth_http_process_headers(ngx_mail_session_t *s, ngx_mail_auth_http_ctx_t *ctx); static void ngx_mail_auth_sleep_handler(ngx_event_t *rev); +static void ngx_mail_auth_send_error(ngx_mail_session_t *s); static ngx_int_t ngx_mail_auth_http_parse_header_line(ngx_mail_session_t *s, ngx_mail_auth_http_ctx_t *ctx); static void ngx_mail_auth_http_block_read(ngx_event_t *rev); @@ -152,6 +154,8 @@ static ngx_str_t ngx_mail_auth_http_me ngx_string("apop"), ngx_string("cram-md5"), ngx_string("external"), + ngx_string("xoauth2"), + ngx_string("oauthbearer"), ngx_string("none") }; @@ -677,6 +681,51 @@ ngx_mail_auth_http_process_headers(ngx_m continue; } + if (len == sizeof("Auth-Error-SASL") - 1 + && ngx_strncasecmp(ctx->header_name_start, + (u_char *) "Auth-Error-SASL", + sizeof("Auth-Error-SASL") - 1) + == 0) + { + if (s->auth_method != NGX_MAIL_AUTH_XOAUTH2 + && s->auth_method != NGX_MAIL_AUTH_OAUTHBEARER) + { + continue; + } + + len = ctx->header_end - ctx->header_start; + + if (s->protocol == NGX_MAIL_SMTP_PROTOCOL) { + size = len + sizeof("334 " CRLF) - 1; + + } else { + size = len + sizeof("+ " CRLF) - 1; + } + + p = ngx_pnalloc(s->connection->pool, size); + if (p == NULL) { + ngx_close_connection(ctx->peer.connection); + ngx_destroy_pool(ctx->pool); + ngx_mail_session_internal_server_error(s); + return; + } + + ctx->errsasl.len = size; + ctx->errsasl.data = p; + + if (s->protocol == NGX_MAIL_SMTP_PROTOCOL) { + *p++ = '3'; *p++ = '3'; *p++ = '4'; *p++ = ' '; + + } else { + *p++ = '+'; *p++ = ' '; + } + + p = ngx_cpymem(p, ctx->header_start, len); + *p++ = CR; *p = LF; + + continue; + } + /* ignore other headers */ continue; @@ -717,14 +766,15 @@ ngx_mail_auth_http_process_headers(ngx_m *p++ = CR; *p = LF; } - s->out = ctx->err; + s->out = ctx->errsasl; + s->auth_err = ctx->err; timer = ctx->sleep; ngx_destroy_pool(ctx->pool); if (timer == 0) { - s->quit = 1; - ngx_mail_send(s->connection->write); + s->auth_quit = 1; + ngx_mail_auth_send_error(s); return; } @@ -858,9 +908,8 @@ ngx_mail_auth_http_process_headers(ngx_m static void ngx_mail_auth_sleep_handler(ngx_event_t *rev) { - ngx_connection_t *c; - ngx_mail_session_t *s; - ngx_mail_core_srv_conf_t *cscf; + ngx_connection_t *c; + ngx_mail_session_t *s; ngx_log_debug0(NGX_LOG_DEBUG_MAIL, rev->log, 0, "mail auth sleep handler"); @@ -877,33 +926,7 @@ ngx_mail_auth_sleep_handler(ngx_event_t return; } - cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module); - - rev->handler = cscf->protocol->auth_state; - - s->mail_state = 0; - s->auth_method = NGX_MAIL_AUTH_PLAIN; - s->tag.len = 0; - - c->log->action = "in auth state"; - - ngx_mail_send(c->write); - - if (c->destroyed) { - return; - } - - ngx_add_timer(rev, cscf->timeout); - - if (rev->ready) { - rev->handler(rev); - return; - } - - if (ngx_handle_read_event(rev, 0) != NGX_OK) { - ngx_mail_close_connection(c); - } - + ngx_mail_auth_send_error(s); return; } @@ -915,6 +938,57 @@ ngx_mail_auth_sleep_handler(ngx_event_t } +static void +ngx_mail_auth_send_error(ngx_mail_session_t *s) +{ + ngx_event_t *rev; + ngx_connection_t *c; + ngx_mail_core_srv_conf_t *cscf; + + c = s->connection; + rev = c->read; + + cscf = ngx_mail_get_module_srv_conf(s, ngx_mail_core_module); + + rev->handler = cscf->protocol->auth_state; + + s->auth_method = NGX_MAIL_AUTH_PLAIN; + + c->log->action = "in auth state"; + + if (s->out.len == 0) { + s->out = s->auth_err; + s->quit = s->auth_quit; + ngx_str_null(&s->auth_err); + + s->state = 0; + s->mail_state = 0; + s->tag.len = 0; + + } else { + s->auth_err.len -= s->tag.len; + s->auth_err.data += s->tag.len; + } + + ngx_mail_send(c->write); + + if (c->destroyed) { + return; + } + + ngx_add_timer(rev, cscf->timeout); + + if (rev->ready) { + rev->handler(rev); + return; + } + + if (ngx_handle_read_event(rev, 0) != NGX_OK) { + ngx_mail_close_connection(c); + } +} + + static ngx_int_t ngx_mail_auth_http_parse_header_line(ngx_mail_session_t *s, ngx_mail_auth_http_ctx_t *ctx) diff --git a/src/mail/ngx_mail_handler.c b/src/mail/ngx_mail_handler.c --- a/src/mail/ngx_mail_handler.c +++ b/src/mail/ngx_mail_handler.c @@ -755,6 +755,274 @@ ngx_mail_auth_external(ngx_mail_session_ } +ngx_int_t +ngx_mail_auth_xoauth2(ngx_mail_session_t *s, ngx_connection_t *c, ngx_uint_t n) +{ + u_char *p, *last; + ngx_str_t *arg, oauth; + + arg = s->args.elts; + + if (s->auth_err.len) { + ngx_log_debug0(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth xoauth2 cancel"); + + if (s->args.nelts == 1 && arg[0].len == 0) { + s->out = s->auth_err; + s->quit = s->auth_quit; + s->state = 0; + s->mail_state = 0; + ngx_str_null(&s->auth_err); + return NGX_OK; + } + + s->quit = s->auth_quit; + ngx_str_null(&s->auth_err); + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth xoauth2: \"%V\"", &arg[n]); + + oauth.data = ngx_pnalloc(c->pool, ngx_base64_decoded_length(arg[n].len)); + if (oauth.data == NULL) { + return NGX_ERROR; + } + + if (ngx_decode_base64(&oauth, &arg[n]) != NGX_OK) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid base64 encoding in " + "AUTH XOAUTH2 command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + /* + * https://developers.google.com/gmail/imap/xoauth2-protocol + * "user=" {User} "^Aauth=Bearer " {token} "^A^A" + */ + + p = oauth.data; + last = p + oauth.len; + + while (p < last) { + if (*p++ == '\1') { + s->login.len = p - oauth.data - 1; + s->login.data = oauth.data; + s->passwd.len = last - p; + s->passwd.data = p; + break; + } + } + + if (s->login.len < sizeof("user=") - 1 + || ngx_strncasecmp(s->login.data, (u_char *) "user=", + sizeof("user=") - 1) + != 0) + { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid login in AUTH XOAUTH2 command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + s->login.len -= sizeof("user=") - 1; + s->login.data += sizeof("user=") - 1; + + if (s->passwd.len < sizeof("auth=Bearer ") - 1 + || ngx_strncasecmp(s->passwd.data, (u_char *) "auth=Bearer ", + sizeof("auth=Bearer ") - 1) + != 0) + { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid token in AUTH XOAUTH2 command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + s->passwd.len -= sizeof("auth=Bearer ") - 1; + s->passwd.data += sizeof("auth=Bearer ") - 1; + + if (s->passwd.len < 2 + || s->passwd.data[s->passwd.len - 2] != '\1' + || s->passwd.data[s->passwd.len - 1] != '\1') + { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid token in AUTH XOAUTH2 command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + s->passwd.len -= 2; + + ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth xoauth2: \"%V\" \"%V\"", &s->login, &s->passwd); + + s->auth_method = NGX_MAIL_AUTH_XOAUTH2; + + return NGX_DONE; +} + + +ngx_int_t +ngx_mail_auth_oauthbearer(ngx_mail_session_t *s, ngx_connection_t *c, + ngx_uint_t n) +{ + u_char *p, *d, *last, *prev; + ngx_str_t *arg, oauth; + + arg = s->args.elts; + + if (s->auth_err.len) { + ngx_log_debug0(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth oauthbearer cancel"); + + if (s->args.nelts == 1 + && ngx_strncmp(arg[0].data, (u_char *) "AQ==", 4) == 0) + { + s->out = s->auth_err; + s->quit = s->auth_quit; + s->state = 0; + s->mail_state = 0; + ngx_str_null(&s->auth_err); + return NGX_OK; + } + + s->quit = s->auth_quit; + ngx_str_null(&s->auth_err); + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + ngx_log_debug1(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth oauthbearer: \"%V\"", &arg[n]); + + oauth.data = ngx_pnalloc(c->pool, ngx_base64_decoded_length(arg[n].len)); + if (oauth.data == NULL) { + return NGX_ERROR; + } + + if (ngx_decode_base64(&oauth, &arg[n]) != NGX_OK) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid base64 encoding in " + "AUTH OAUTHBEARER command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + /* + * RFC 7628 + * "n,a=user at example.com,^A...^Aauth=Bearer ^A^A" + */ + + p = oauth.data; + last = p + oauth.len; + + s->login.len = 0; + prev = NULL; + + while (p < last) { + if (*p == ',') { + if (prev + && (size_t) (p - prev) > sizeof("a=") - 1 + && ngx_strncasecmp(prev, (u_char *) "a=", sizeof("a=") - 1) + == 0) + { + s->login.len = p - prev - (sizeof("a=") - 1); + s->login.data = prev + sizeof("a=") - 1; + break; + } + + p++; + prev = p; + continue; + } + + if (*p == '\1') { + break; + } + + p++; + } + + if (s->login.len == 0) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid login in AUTH OAUTHBEARER command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + s->passwd.len = 0; + prev = NULL; + + while (p < last) { + if (*p == '\1') { + if (prev + && (size_t) (p - prev) > sizeof("auth=Bearer ") - 1 + && ngx_strncasecmp(prev, (u_char *) "auth=Bearer ", + sizeof("auth=Bearer ") - 1) + == 0) + { + s->passwd.len = p - prev - (sizeof("auth=Bearer ") - 1); + s->passwd.data = prev + sizeof("auth=Bearer ") - 1; + break; + } + + p++; + prev = p; + continue; + } + + p++; + } + + if (s->passwd.len == 0) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid token in AUTH OAUTHBEARER command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + /* decode =2C =3D in login */ + + p = s->login.data; + d = s->login.data; + last = s->login.data + s->login.len; + + while (p < last) { + if (*p == '=') { + + /* + * login is always followed by other data, + * so p[1] and p[2] can be checked directly + */ + + if (p[1] == '2' && (p[2] == 'C' || p[2] == 'c')) { + *d++ = ','; + + } else if (p[1] == '3' && (p[2] == 'D' || p[2] == 'd')) { + *d++ = '='; + + } else { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "client sent invalid login in " + "AUTH OAUTHBEARER command"); + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + p += 3; + continue; + } + + *d++ = *p++; + } + + s->login.len = d - s->login.data; + + ngx_log_debug2(NGX_LOG_DEBUG_MAIL, c->log, 0, + "mail auth oauthbearer: \"%V\" \"%V\"", + &s->login, &s->passwd); + + s->auth_method = NGX_MAIL_AUTH_OAUTHBEARER; + + return NGX_DONE; +} + + void ngx_mail_send(ngx_event_t *wev) { @@ -919,13 +1187,17 @@ ngx_mail_auth(ngx_mail_session_t *s, ngx { s->args.nelts = 0; - if (s->buffer->pos == s->buffer->last) { - s->buffer->pos = s->buffer->start; - s->buffer->last = s->buffer->start; + if (s->state) { + /* preserve tag */ + s->arg_start = s->buffer->pos; + + } else { + if (s->buffer->pos == s->buffer->last) { + s->buffer->pos = s->buffer->start; + s->buffer->last = s->buffer->start; + } } - s->state = 0; - if (c->read->timer_set) { ngx_del_timer(c->read); } diff --git a/src/mail/ngx_mail_imap_handler.c b/src/mail/ngx_mail_imap_handler.c --- a/src/mail/ngx_mail_imap_handler.c +++ b/src/mail/ngx_mail_imap_handler.c @@ -220,6 +220,14 @@ ngx_mail_imap_auth_state(ngx_event_t *re case ngx_imap_auth_external: rc = ngx_mail_auth_external(s, c, 0); break; + + case ngx_imap_auth_xoauth2: + rc = ngx_mail_auth_xoauth2(s, c, 0); + break; + + case ngx_imap_auth_oauthbearer: + rc = ngx_mail_auth_oauthbearer(s, c, 0); + break; } } else if (rc == NGX_IMAP_NEXT) { @@ -432,6 +440,38 @@ ngx_mail_imap_authenticate(ngx_mail_sess s->mail_state = ngx_imap_auth_external; return NGX_OK; + + case NGX_MAIL_AUTH_XOAUTH2: + + if (!(iscf->auth_methods & NGX_MAIL_AUTH_XOAUTH2_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_imap_auth_xoauth2; + return ngx_mail_auth_xoauth2(s, c, 1); + } + + ngx_str_set(&s->out, imap_plain_next); + s->mail_state = ngx_imap_auth_xoauth2; + + return NGX_OK; + + case NGX_MAIL_AUTH_OAUTHBEARER: + + if (!(iscf->auth_methods & NGX_MAIL_AUTH_OAUTHBEARER_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_imap_auth_oauthbearer; + return ngx_mail_auth_oauthbearer(s, c, 1); + } + + ngx_str_set(&s->out, imap_plain_next); + s->mail_state = ngx_imap_auth_oauthbearer; + + return NGX_OK; } return rc; diff --git a/src/mail/ngx_mail_imap_module.c b/src/mail/ngx_mail_imap_module.c --- a/src/mail/ngx_mail_imap_module.c +++ b/src/mail/ngx_mail_imap_module.c @@ -30,6 +30,8 @@ static ngx_conf_bitmask_t ngx_mail_imap { ngx_string("login"), NGX_MAIL_AUTH_LOGIN_ENABLED }, { ngx_string("cram-md5"), NGX_MAIL_AUTH_CRAM_MD5_ENABLED }, { ngx_string("external"), NGX_MAIL_AUTH_EXTERNAL_ENABLED }, + { ngx_string("xoauth2"), NGX_MAIL_AUTH_XOAUTH2_ENABLED }, + { ngx_string("oauthbearer"), NGX_MAIL_AUTH_OAUTHBEARER_ENABLED }, { ngx_null_string, 0 } }; @@ -40,6 +42,8 @@ static ngx_str_t ngx_mail_imap_auth_met ngx_null_string, /* APOP */ ngx_string("AUTH=CRAM-MD5"), ngx_string("AUTH=EXTERNAL"), + ngx_string("AUTH=XOAUTH2"), + ngx_string("AUTH=OAUTHBEARER"), ngx_null_string /* NONE */ }; @@ -182,7 +186,7 @@ ngx_mail_imap_merge_srv_conf(ngx_conf_t } for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { @@ -208,7 +212,7 @@ ngx_mail_imap_merge_srv_conf(ngx_conf_t auth = p; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { diff --git a/src/mail/ngx_mail_parse.c b/src/mail/ngx_mail_parse.c --- a/src/mail/ngx_mail_parse.c +++ b/src/mail/ngx_mail_parse.c @@ -953,6 +953,20 @@ ngx_mail_auth_parse(ngx_mail_session_t * return NGX_MAIL_PARSE_INVALID_COMMAND; } + if (arg[0].len == 7) { + + if (ngx_strncasecmp(arg[0].data, (u_char *) "XOAUTH2", 7) == 0) { + + if (s->args.nelts == 1 || s->args.nelts == 2) { + return NGX_MAIL_AUTH_XOAUTH2; + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + if (arg[0].len == 8) { if (ngx_strncasecmp(arg[0].data, (u_char *) "CRAM-MD5", 8) == 0) { @@ -976,5 +990,19 @@ ngx_mail_auth_parse(ngx_mail_session_t * return NGX_MAIL_PARSE_INVALID_COMMAND; } + if (arg[0].len == 11) { + + if (ngx_strncasecmp(arg[0].data, (u_char *) "OAUTHBEARER", 11) == 0) { + + if (s->args.nelts == 1 || s->args.nelts == 2) { + return NGX_MAIL_AUTH_OAUTHBEARER; + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + return NGX_MAIL_PARSE_INVALID_COMMAND; } diff --git a/src/mail/ngx_mail_pop3_handler.c b/src/mail/ngx_mail_pop3_handler.c --- a/src/mail/ngx_mail_pop3_handler.c +++ b/src/mail/ngx_mail_pop3_handler.c @@ -260,6 +260,14 @@ ngx_mail_pop3_auth_state(ngx_event_t *re case ngx_pop3_auth_external: rc = ngx_mail_auth_external(s, c, 0); break; + + case ngx_pop3_auth_xoauth2: + rc = ngx_mail_auth_xoauth2(s, c, 0); + break; + + case ngx_pop3_auth_oauthbearer: + rc = ngx_mail_auth_oauthbearer(s, c, 0); + break; } } @@ -553,6 +561,38 @@ ngx_mail_pop3_auth(ngx_mail_session_t *s s->mail_state = ngx_pop3_auth_external; return NGX_OK; + + case NGX_MAIL_AUTH_XOAUTH2: + + if (!(pscf->auth_methods & NGX_MAIL_AUTH_XOAUTH2_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_pop3_auth_xoauth2; + return ngx_mail_auth_xoauth2(s, c, 1); + } + + ngx_str_set(&s->out, pop3_next); + s->mail_state = ngx_pop3_auth_xoauth2; + + return NGX_OK; + + case NGX_MAIL_AUTH_OAUTHBEARER: + + if (!(pscf->auth_methods & NGX_MAIL_AUTH_OAUTHBEARER_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_pop3_auth_oauthbearer; + return ngx_mail_auth_oauthbearer(s, c, 1); + } + + ngx_str_set(&s->out, pop3_next); + s->mail_state = ngx_pop3_auth_oauthbearer; + + return NGX_OK; } return rc; diff --git a/src/mail/ngx_mail_pop3_module.c b/src/mail/ngx_mail_pop3_module.c --- a/src/mail/ngx_mail_pop3_module.c +++ b/src/mail/ngx_mail_pop3_module.c @@ -30,6 +30,8 @@ static ngx_conf_bitmask_t ngx_mail_pop3 { ngx_string("apop"), NGX_MAIL_AUTH_APOP_ENABLED }, { ngx_string("cram-md5"), NGX_MAIL_AUTH_CRAM_MD5_ENABLED }, { ngx_string("external"), NGX_MAIL_AUTH_EXTERNAL_ENABLED }, + { ngx_string("xoauth2"), NGX_MAIL_AUTH_XOAUTH2_ENABLED }, + { ngx_string("oauthbearer"), NGX_MAIL_AUTH_OAUTHBEARER_ENABLED }, { ngx_null_string, 0 } }; @@ -40,6 +42,8 @@ static ngx_str_t ngx_mail_pop3_auth_met ngx_null_string, /* APOP */ ngx_string("CRAM-MD5"), ngx_string("EXTERNAL"), + ngx_string("XOAUTH2"), + ngx_string("OAUTHBEARER"), ngx_null_string /* NONE */ }; @@ -183,7 +187,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t size += sizeof("SASL") - 1 + sizeof(CRLF) - 1; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { @@ -214,7 +218,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t p = ngx_cpymem(p, "SASL", sizeof("SASL") - 1); for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { @@ -254,7 +258,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t + sizeof("." CRLF) - 1; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { @@ -279,7 +283,7 @@ ngx_mail_pop3_merge_srv_conf(ngx_conf_t sizeof("+OK methods supported:" CRLF) - 1); for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (ngx_mail_pop3_auth_methods_names[i].len == 0) { diff --git a/src/mail/ngx_mail_smtp_handler.c b/src/mail/ngx_mail_smtp_handler.c --- a/src/mail/ngx_mail_smtp_handler.c +++ b/src/mail/ngx_mail_smtp_handler.c @@ -548,6 +548,14 @@ ngx_mail_smtp_auth_state(ngx_event_t *re case ngx_smtp_auth_external: rc = ngx_mail_auth_external(s, c, 0); break; + + case ngx_smtp_auth_xoauth2: + rc = ngx_mail_auth_xoauth2(s, c, 0); + break; + + case ngx_smtp_auth_oauthbearer: + rc = ngx_mail_auth_oauthbearer(s, c, 0); + break; } } @@ -745,6 +753,38 @@ ngx_mail_smtp_auth(ngx_mail_session_t *s s->mail_state = ngx_smtp_auth_external; return NGX_OK; + + case NGX_MAIL_AUTH_XOAUTH2: + + if (!(sscf->auth_methods & NGX_MAIL_AUTH_XOAUTH2_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_smtp_auth_xoauth2; + return ngx_mail_auth_xoauth2(s, c, 1); + } + + ngx_str_set(&s->out, smtp_next); + s->mail_state = ngx_smtp_auth_xoauth2; + + return NGX_OK; + + case NGX_MAIL_AUTH_OAUTHBEARER: + + if (!(sscf->auth_methods & NGX_MAIL_AUTH_OAUTHBEARER_ENABLED)) { + return NGX_MAIL_PARSE_INVALID_COMMAND; + } + + if (s->args.nelts == 2) { + s->mail_state = ngx_smtp_auth_oauthbearer; + return ngx_mail_auth_oauthbearer(s, c, 1); + } + + ngx_str_set(&s->out, smtp_next); + s->mail_state = ngx_smtp_auth_oauthbearer; + + return NGX_OK; } return rc; diff --git a/src/mail/ngx_mail_smtp_module.c b/src/mail/ngx_mail_smtp_module.c --- a/src/mail/ngx_mail_smtp_module.c +++ b/src/mail/ngx_mail_smtp_module.c @@ -22,6 +22,8 @@ static ngx_conf_bitmask_t ngx_mail_smtp { ngx_string("login"), NGX_MAIL_AUTH_LOGIN_ENABLED }, { ngx_string("cram-md5"), NGX_MAIL_AUTH_CRAM_MD5_ENABLED }, { ngx_string("external"), NGX_MAIL_AUTH_EXTERNAL_ENABLED }, + { ngx_string("xoauth2"), NGX_MAIL_AUTH_XOAUTH2_ENABLED }, + { ngx_string("oauthbearer"), NGX_MAIL_AUTH_OAUTHBEARER_ENABLED }, { ngx_string("none"), NGX_MAIL_AUTH_NONE_ENABLED }, { ngx_null_string, 0 } }; @@ -33,6 +35,8 @@ static ngx_str_t ngx_mail_smtp_auth_met ngx_null_string, /* APOP */ ngx_string("CRAM-MD5"), ngx_string("EXTERNAL"), + ngx_string("XOAUTH2"), + ngx_string("OAUTHBEARER"), ngx_null_string /* NONE */ }; @@ -210,7 +214,7 @@ ngx_mail_smtp_merge_srv_conf(ngx_conf_t auth_enabled = 0; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { @@ -253,7 +257,7 @@ ngx_mail_smtp_merge_srv_conf(ngx_conf_t *p++ = 'A'; *p++ = 'U'; *p++ = 'T'; *p++ = 'H'; for (m = NGX_MAIL_AUTH_PLAIN_ENABLED, i = 0; - m <= NGX_MAIL_AUTH_EXTERNAL_ENABLED; + m < NGX_MAIL_AUTH_NONE_ENABLED; m <<= 1, i++) { if (m & conf->auth_methods) { -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Fri May 31 23:14:41 2024 From: mdounin at mdounin.ru (Maxim Dounin) Date: Sat, 1 Jun 2024 02:14:41 +0300 Subject: proxy_cache_lock for content revalidation In-Reply-To: References: Message-ID: Hello! On Fri, May 31, 2024 at 11:25:07AM -0700, Maksim Yevmenkin wrote: > [..] > > > > > > > https://mailman.nginx.org/pipermail/nginx-devel/2018-December/011710.html > > > > thank you! it seems the original post mentioned this exact issue. it > > also seems that the patch was removed. i am curious if it would be > > possible to restore the patch. > > please never mind, i'm blind, sorry! Yep, the patch is there. It is however linked with a plain http link, which prevents at least Chrome from loading it unless requested separately. -- Maxim Dounin http://mdounin.ru/