From mdounin at mdounin.ru Fri Aug 1 15:42:48 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 01 Aug 2025 18:42:48 +0300 Subject: [nginx-site] Removed ntlm links, missed in 3043:9eadb98ec770. Message-ID: details: http://freenginx.org/hg/nginx-site/rev/de1ca6140778 branches: changeset: 3114:de1ca6140778 user: Maxim Dounin date: Fri Aug 01 18:41:53 2025 +0300 description: Removed ntlm links, missed in 3043:9eadb98ec770. diffstat: xml/en/docs/http/ngx_http_proxy_module.xml | 5 ++--- xml/ru/docs/http/ngx_http_proxy_module.xml | 6 ++---- 2 files changed, 4 insertions(+), 7 deletions(-) diffs (45 lines): diff --git a/xml/en/docs/http/ngx_http_proxy_module.xml b/xml/en/docs/http/ngx_http_proxy_module.xml --- a/xml/en/docs/http/ngx_http_proxy_module.xml +++ b/xml/en/docs/http/ngx_http_proxy_module.xml @@ -10,7 +10,7 @@ + rev="79">
@@ -980,8 +980,7 @@ Sets the HTTP protocol version for proxy By default, version 1.0 is used. Version 1.1 is recommended for use with -connections and -NTLM authentication. +connections. diff --git a/xml/ru/docs/http/ngx_http_proxy_module.xml b/xml/ru/docs/http/ngx_http_proxy_module.xml --- a/xml/ru/docs/http/ngx_http_proxy_module.xml +++ b/xml/ru/docs/http/ngx_http_proxy_module.xml @@ -10,7 +10,7 @@ + rev="79">
@@ -980,9 +980,7 @@ nginx ?? ???????? ??????? ???? ?????????
Date
, ?? ????????? ???????????? ?????? 1.0. ??? ?????? ?????????? -?????????? ? -???????? ??????????? -NTLM ????????????? ?????? 1.1. +?????????? ????????????? ?????? 1.1. From mdounin at mdounin.ru Fri Aug 8 20:08:55 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:08:55 +0300 Subject: [PATCH 00 of 12] handling of 1xx upstream responses Message-ID: Hello! The following patch series introduces minimal handling of 1xx interim upstream responses: such responses are now either rejected (101 Switching Protocols if not requested or not supported by the protocol) or ignored (other 1xx responses). Additionally, the patch series does various related changes. Most notably, HTTP/0.9 responses are now rejected by default. Handling of HTTP/0.9 responses can be re-enabled with the "proxy_allow_http09" directive. Review and testing appreciated. -- Maxim Dounin From mdounin at mdounin.ru Fri Aug 8 20:08:56 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:08:56 +0300 Subject: [PATCH 01 of 12] Proxy: replaced some r->upstream usage with the "u" variable In-Reply-To: References: Message-ID: <3b2af53cea942782c3ae.1754683736@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1754683240 -10800 # Fri Aug 08 23:00:40 2025 +0300 # Node ID 3b2af53cea942782c3aed07279a88c2d6ceef7c7 # Parent bdfd605f661eea3d272caf1bd5d85e7c539394ca Proxy: replaced some r->upstream usage with the "u" variable. No functional changes. diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c --- a/src/http/modules/ngx_http_proxy_module.c +++ b/src/http/modules/ngx_http_proxy_module.c @@ -1958,13 +1958,15 @@ ngx_http_proxy_process_header(ngx_http_r ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http proxy header done"); + u = r->upstream; + /* * if no "Server" and "Date" in header line, * then add the special empty headers */ - if (r->upstream->headers_in.server == NULL) { - h = ngx_list_push(&r->upstream->headers_in.headers); + if (u->headers_in.server == NULL) { + h = ngx_list_push(&u->headers_in.headers); if (h == NULL) { return NGX_ERROR; } @@ -1978,8 +1980,8 @@ ngx_http_proxy_process_header(ngx_http_r h->next = NULL; } - if (r->upstream->headers_in.date == NULL) { - h = ngx_list_push(&r->upstream->headers_in.headers); + if (u->headers_in.date == NULL) { + h = ngx_list_push(&u->headers_in.headers); if (h == NULL) { return NGX_ERROR; } @@ -1994,8 +1996,6 @@ ngx_http_proxy_process_header(ngx_http_r /* clear content length if response is chunked */ - u = r->upstream; - if (u->headers_in.chunked) { u->headers_in.content_length_n = -1; } From mdounin at mdounin.ru Fri Aug 8 20:08:57 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:08:57 +0300 Subject: [PATCH 02 of 12] Proxy: reworked HTTP/0.9 handling In-Reply-To: References: Message-ID: <2bdee8408a2305c96e4a.1754683737@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1754683242 -10800 # Fri Aug 08 23:00:42 2025 +0300 # Node ID 2bdee8408a2305c96e4a8e6ada355228a1ac62bc # Parent 3b2af53cea942782c3aed07279a88c2d6ceef7c7 Proxy: reworked HTTP/0.9 handling. Previous behaviour was to downgrade the client connection to HTTP/0.9 and return the upstream response as is. However, this approach doesn't really work with HTTP/2, and will break HTTP/3 as currently implemented (as it relies on r->http_version). Further, this approach is potentially unsafe: if a status line is not recognized as HTTP/1.x by nginx, but accepted by the client, the response can be recognized as multiple responses. Now, instead of downgrading client connection to HTTP/0.9, we generate a 200 response with the response body received via HTTP/0.9, much like the memcached module does. Additionally, now logging is done consistently at the "debug" level, and special handling for cache is removed. This resolves various issues with cache enabled, such as missing status in $upstream_status and no logging at all. diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c --- a/src/http/modules/ngx_http_proxy_module.c +++ b/src/http/modules/ngx_http_proxy_module.c @@ -1826,28 +1826,16 @@ ngx_http_proxy_process_status_line(ngx_h if (rc == NGX_ERROR) { -#if (NGX_HTTP_CACHE) - - if (r->cache) { - r->http_version = NGX_HTTP_VERSION_9; - return NGX_OK; + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http proxy no HTTP/1.0 header"); + + u->headers_in.status_n = 200; + u->headers_in.connection_close = 1; + + if (u->state && u->state->status == 0) { + u->state->status = NGX_HTTP_OK; } -#endif - - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, - "upstream sent no valid HTTP/1.0 header"); - -#if 0 - if (u->accel) { - return NGX_HTTP_UPSTREAM_INVALID_HEADER; - } -#endif - - r->http_version = NGX_HTTP_VERSION_9; - u->state->status = NGX_HTTP_OK; - u->headers_in.connection_close = 1; - return NGX_OK; } From mdounin at mdounin.ru Fri Aug 8 20:08:58 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:08:58 +0300 Subject: [PATCH 03 of 12] Proxy: HTTP/0.9 responses now disabled by default In-Reply-To: References: Message-ID: <533246d275487cfbc511.1754683738@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1754683245 -10800 # Fri Aug 08 23:00:45 2025 +0300 # Node ID 533246d275487cfbc51111e7ee05d0c014dd7d78 # Parent 2bdee8408a2305c96e4a8e6ada355228a1ac62bc Proxy: HTTP/0.9 responses now disabled by default. Compatibility with HTTP/0.9 is no longer essential nowadays. Further, it often causes confusion when a malformed HTTP/1.x response is interpreted as a HTTP/0.9 response, or when proxying to a non-HTTP server appears to work and return something. As such, compatibility with HTTP/0.9 responses in the proxy module is now disabled by default. The "proxy_allow_http09" directive makes it possible to re-enable it if needed in the particular setup. diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c --- a/src/http/modules/ngx_http_proxy_module.c +++ b/src/http/modules/ngx_http_proxy_module.c @@ -111,6 +111,7 @@ typedef struct { ngx_http_proxy_vars_t vars; ngx_flag_t redirect; + ngx_flag_t http09; ngx_uint_t http_version; @@ -686,6 +687,13 @@ static ngx_command_t ngx_http_proxy_com offsetof(ngx_http_proxy_loc_conf_t, http_version), &ngx_http_proxy_http_version }, + { ngx_string("proxy_allow_http09"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_proxy_loc_conf_t, http09), + NULL }, + #if (NGX_HTTP_SSL) { ngx_string("proxy_ssl_session_reuse"), @@ -1805,10 +1813,11 @@ out: static ngx_int_t ngx_http_proxy_process_status_line(ngx_http_request_t *r) { - size_t len; - ngx_int_t rc; - ngx_http_upstream_t *u; - ngx_http_proxy_ctx_t *ctx; + size_t len; + ngx_int_t rc; + ngx_http_upstream_t *u; + ngx_http_proxy_ctx_t *ctx; + ngx_http_proxy_loc_conf_t *plcf; ctx = ngx_http_get_module_ctx(r, ngx_http_proxy_module); @@ -1829,6 +1838,14 @@ ngx_http_proxy_process_status_line(ngx_h ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http proxy no HTTP/1.0 header"); + plcf = ngx_http_get_module_loc_conf(r, ngx_http_proxy_module); + + if (!plcf->http09) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "upstream sent no valid HTTP/1.0 header"); + return NGX_HTTP_UPSTREAM_INVALID_HEADER; + } + u->headers_in.status_n = 200; u->headers_in.connection_close = 1; @@ -3414,6 +3431,7 @@ ngx_http_proxy_create_loc_conf(ngx_conf_ conf->method = NGX_CONF_UNSET_PTR; conf->redirect = NGX_CONF_UNSET; + conf->http09 = NGX_CONF_UNSET; conf->cookie_domains = NGX_CONF_UNSET_PTR; conf->cookie_paths = NGX_CONF_UNSET_PTR; @@ -3764,6 +3782,7 @@ ngx_http_proxy_merge_loc_conf(ngx_conf_t ngx_conf_merge_ptr_value(conf->method, prev->method, NULL); ngx_conf_merge_value(conf->redirect, prev->redirect, 1); + ngx_conf_merge_value(conf->http09, prev->http09, 0); if (conf->redirect) { From mdounin at mdounin.ru Fri Aug 8 20:08:59 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:08:59 +0300 Subject: [PATCH 04 of 12] Upstream: simplified ngx_http_upstream_process_header() In-Reply-To: References: Message-ID: <367106bfe31eddfa021f.1754683739@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1754683248 -10800 # Fri Aug 08 23:00:48 2025 +0300 # Node ID 367106bfe31eddfa021fab9403fcefdc8243bf72 # Parent 533246d275487cfbc51111e7ee05d0c014dd7d78 Upstream: simplified ngx_http_upstream_process_header(). This restores the simple loop structure which was somewhat screwed up in 9237:41db21d1ca7c, and also removes some commented out artifacts of the past. Note that checking c->read->ready before the first c->recv() is not required, since ngx_http_upstream_process_header() is an event handler and it cannot be called multiple times in a loop. 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 @@ -2469,25 +2469,11 @@ ngx_http_upstream_process_header(ngx_htt for ( ;; ) { - if (c->read->ready) { - n = c->recv(c, u->buffer.last, u->buffer.end - u->buffer.last); - - } else { - n = NGX_AGAIN; - } + n = c->recv(c, u->buffer.last, u->buffer.end - u->buffer.last); if (n == NGX_AGAIN) { -#if 0 - ngx_add_timer(rev, u->read_timeout); -#endif - - if (ngx_handle_read_event(c->read, 0) != NGX_OK) { - ngx_http_upstream_finalize_request(r, u, - NGX_HTTP_INTERNAL_SERVER_ERROR); - return; - } - - return; + rc = NGX_AGAIN; + break; } if (n == 0) { @@ -2501,15 +2487,8 @@ ngx_http_upstream_process_header(ngx_htt } u->state->bytes_received += n; - u->buffer.last += n; -#if 0 - u->valid_header_in = 0; - - u->peer.cached = 0; -#endif - rc = u->process_header(r); if (rc == NGX_AGAIN) { @@ -2523,12 +2502,26 @@ ngx_http_upstream_process_header(ngx_htt return; } + if (!c->read->ready) { + break; + } + continue; } break; } + if (rc == NGX_AGAIN) { + if (ngx_handle_read_event(c->read, 0) != NGX_OK) { + ngx_http_upstream_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + + return; + } + if (rc == NGX_HTTP_UPSTREAM_INVALID_HEADER) { ngx_http_upstream_next(r, u, NGX_HTTP_UPSTREAM_FT_INVALID_HEADER); return; From mdounin at mdounin.ru Fri Aug 8 20:09:00 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:09:00 +0300 Subject: [PATCH 05 of 12] Upstream: simplified u->state->status handling In-Reply-To: References: Message-ID: <8466023cd0bec6808904.1754683740@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1754683251 -10800 # Fri Aug 08 23:00:51 2025 +0300 # Node ID 8466023cd0bec68089048f45e1e4c3985d54678e # Parent 367106bfe31eddfa021fab9403fcefdc8243bf72 Upstream: simplified u->state->status handling. Instead of assigning u->state->status when parsing a status line, and then overwriting it if an error happens, we now assign u->state->status after all input headers are parsed. This simplifies the code, and also avoids additional checks needed to handle parsing the headers from cache. It is also simplifies upcoming changes to handle 1xx interim responses. diff --git a/src/http/modules/ngx_http_fastcgi_module.c b/src/http/modules/ngx_http_fastcgi_module.c --- a/src/http/modules/ngx_http_fastcgi_module.c +++ b/src/http/modules/ngx_http_fastcgi_module.c @@ -2063,10 +2063,6 @@ ngx_http_fastcgi_process_header(ngx_http ngx_str_set(&u->headers_in.status_line, "200 OK"); } - if (u->state && u->state->status == 0) { - u->state->status = u->headers_in.status_n; - } - break; } diff --git a/src/http/modules/ngx_http_grpc_module.c b/src/http/modules/ngx_http_grpc_module.c --- a/src/http/modules/ngx_http_grpc_module.c +++ b/src/http/modules/ngx_http_grpc_module.c @@ -1867,10 +1867,6 @@ ngx_http_grpc_process_header(ngx_http_re u->headers_in.status_n = status; - if (u->state && u->state->status == 0) { - u->state->status = status; - } - ctx->status = 1; continue; diff --git a/src/http/modules/ngx_http_memcached_module.c b/src/http/modules/ngx_http_memcached_module.c --- a/src/http/modules/ngx_http_memcached_module.c +++ b/src/http/modules/ngx_http_memcached_module.c @@ -422,7 +422,6 @@ found: } u->headers_in.status_n = 200; - u->state->status = 200; u->buffer.pos = p + sizeof(CRLF) - 1; return NGX_OK; @@ -434,7 +433,6 @@ found: u->headers_in.content_length_n = 0; u->headers_in.status_n = 404; - u->state->status = 404; u->buffer.pos = p + sizeof("END" CRLF) - 1; u->keepalive = 1; diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c --- a/src/http/modules/ngx_http_proxy_module.c +++ b/src/http/modules/ngx_http_proxy_module.c @@ -1849,17 +1849,9 @@ ngx_http_proxy_process_status_line(ngx_h u->headers_in.status_n = 200; u->headers_in.connection_close = 1; - if (u->state && u->state->status == 0) { - u->state->status = NGX_HTTP_OK; - } - return NGX_OK; } - if (u->state && u->state->status == 0) { - u->state->status = ctx->status.code; - } - u->headers_in.status_n = ctx->status.code; len = ctx->status.end - ctx->status.start; diff --git a/src/http/modules/ngx_http_scgi_module.c b/src/http/modules/ngx_http_scgi_module.c --- a/src/http/modules/ngx_http_scgi_module.c +++ b/src/http/modules/ngx_http_scgi_module.c @@ -1032,10 +1032,6 @@ ngx_http_scgi_process_status_line(ngx_ht return ngx_http_scgi_process_header(r); } - if (u->state && u->state->status == 0) { - u->state->status = status->code; - } - u->headers_in.status_n = status->code; len = status->end - status->start; @@ -1168,10 +1164,6 @@ ngx_http_scgi_process_header(ngx_http_re ngx_str_set(&u->headers_in.status_line, "200 OK"); } - if (u->state && u->state->status == 0) { - u->state->status = u->headers_in.status_n; - } - done: if (u->headers_in.status_n == NGX_HTTP_SWITCHING_PROTOCOLS diff --git a/src/http/modules/ngx_http_uwsgi_module.c b/src/http/modules/ngx_http_uwsgi_module.c --- a/src/http/modules/ngx_http_uwsgi_module.c +++ b/src/http/modules/ngx_http_uwsgi_module.c @@ -1261,10 +1261,6 @@ ngx_http_uwsgi_process_status_line(ngx_h return ngx_http_uwsgi_process_header(r); } - if (u->state && u->state->status == 0) { - u->state->status = status->code; - } - u->headers_in.status_n = status->code; len = status->end - status->start; @@ -1397,10 +1393,6 @@ ngx_http_uwsgi_process_header(ngx_http_r ngx_str_set(&u->headers_in.status_line, "200 OK"); } - if (u->state && u->state->status == 0) { - u->state->status = u->headers_in.status_n; - } - done: if (u->headers_in.status_n == NGX_HTTP_SWITCHING_PROTOCOLS 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 @@ -2535,6 +2535,7 @@ ngx_http_upstream_process_header(ngx_htt /* rc == NGX_OK */ + u->state->status = u->headers_in.status_n; u->state->header_time = ngx_current_msec - u->start_time; if (u->headers_in.status_n >= NGX_HTTP_SPECIAL_RESPONSE) { From mdounin at mdounin.ru Fri Aug 8 20:09:01 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:09:01 +0300 Subject: [PATCH 06 of 12] Upstream: added a function to initialize and clear input headers In-Reply-To: References: Message-ID: <924c082b02a4afed6910.1754683741@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1754683253 -10800 # Fri Aug 08 23:00:53 2025 +0300 # Node ID 924c082b02a4afed6910650d6ee3b709244a0fba # Parent 8466023cd0bec68089048f45e1e4c3985d54678e Upstream: added a function to initialize and clear input headers. When headers are already initialized, we now reinitialize corresponding ngx_list_t members to clear the structure instead of reinitializing the whole structure (and allocating memory for elements of the first list part), similarly to ngx_http_clean_header(). 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 @@ -45,6 +45,8 @@ static void ngx_http_upstream_connect(ng ngx_http_upstream_t *u); static ngx_int_t ngx_http_upstream_reinit(ngx_http_request_t *r, ngx_http_upstream_t *u); +static ngx_int_t ngx_http_upstream_clear_headers(ngx_http_request_t *r, + ngx_http_upstream_t *u); static void ngx_http_upstream_send_request(ngx_http_request_t *r, ngx_http_upstream_t *u, ngx_uint_t do_write); static ngx_int_t ngx_http_upstream_send_request_body(ngx_http_request_t *r, @@ -519,9 +521,6 @@ ngx_http_upstream_create(ngx_http_reques r->cache = NULL; #endif - u->headers_in.content_length_n = -1; - u->headers_in.last_modified_time = -1; - return NGX_OK; } @@ -1087,21 +1086,7 @@ ngx_http_upstream_cache_send(ngx_http_re u->buffer = *c->buf; u->buffer.pos += c->header_start; - ngx_memzero(&u->headers_in, sizeof(ngx_http_upstream_headers_in_t)); - u->headers_in.content_length_n = -1; - u->headers_in.last_modified_time = -1; - - if (ngx_list_init(&u->headers_in.headers, r->pool, 8, - sizeof(ngx_table_elt_t)) - != NGX_OK) - { - return NGX_ERROR; - } - - if (ngx_list_init(&u->headers_in.trailers, r->pool, 2, - sizeof(ngx_table_elt_t)) - != NGX_OK) - { + if (ngx_http_upstream_clear_headers(r, u) != NGX_OK) { return NGX_ERROR; } @@ -2027,21 +2012,7 @@ ngx_http_upstream_reinit(ngx_http_reques u->upgrade = 0; u->error = 0; - ngx_memzero(&u->headers_in, sizeof(ngx_http_upstream_headers_in_t)); - u->headers_in.content_length_n = -1; - u->headers_in.last_modified_time = -1; - - if (ngx_list_init(&u->headers_in.headers, r->pool, 8, - sizeof(ngx_table_elt_t)) - != NGX_OK) - { - return NGX_ERROR; - } - - if (ngx_list_init(&u->headers_in.trailers, r->pool, 2, - sizeof(ngx_table_elt_t)) - != NGX_OK) - { + if (ngx_http_upstream_clear_headers(r, u) != NGX_OK) { return NGX_ERROR; } @@ -2099,6 +2070,54 @@ ngx_http_upstream_reinit(ngx_http_reques } +static ngx_int_t +ngx_http_upstream_clear_headers(ngx_http_request_t *r, ngx_http_upstream_t *u) +{ + if (u->headers_in.headers.last) { + + /* clear headers and reinitialize lists */ + + ngx_memzero(&u->headers_in.status_n, + sizeof(ngx_http_upstream_headers_in_t) + - offsetof(ngx_http_upstream_headers_in_t, status_n)); + + u->headers_in.headers.part.nelts = 0; + u->headers_in.headers.part.next = NULL; + u->headers_in.headers.last = &u->headers_in.headers.part; + + u->headers_in.trailers.part.nelts = 0; + u->headers_in.trailers.part.next = NULL; + u->headers_in.trailers.last = &u->headers_in.trailers.part; + + u->headers_in.content_length_n = -1; + u->headers_in.last_modified_time = -1; + + return NGX_OK; + } + + /* initialize headers */ + + if (ngx_list_init(&u->headers_in.headers, r->pool, 8, + sizeof(ngx_table_elt_t)) + != NGX_OK) + { + return NGX_ERROR; + } + + if (ngx_list_init(&u->headers_in.trailers, r->pool, 2, + sizeof(ngx_table_elt_t)) + != NGX_OK) + { + return NGX_ERROR; + } + + u->headers_in.content_length_n = -1; + u->headers_in.last_modified_time = -1; + + return NGX_OK; +} + + static void ngx_http_upstream_send_request(ngx_http_request_t *r, ngx_http_upstream_t *u, ngx_uint_t do_write) @@ -2440,24 +2459,6 @@ ngx_http_upstream_process_header(ngx_htt u->buffer.tag = u->output.tag; - if (ngx_list_init(&u->headers_in.headers, r->pool, 8, - sizeof(ngx_table_elt_t)) - != NGX_OK) - { - ngx_http_upstream_finalize_request(r, u, - NGX_HTTP_INTERNAL_SERVER_ERROR); - return; - } - - if (ngx_list_init(&u->headers_in.trailers, r->pool, 2, - sizeof(ngx_table_elt_t)) - != NGX_OK) - { - ngx_http_upstream_finalize_request(r, u, - NGX_HTTP_INTERNAL_SERVER_ERROR); - return; - } - #if (NGX_HTTP_CACHE) if (r->cache) { @@ -2465,6 +2466,12 @@ ngx_http_upstream_process_header(ngx_htt u->buffer.last = u->buffer.pos; } #endif + + if (ngx_http_upstream_clear_headers(r, u) != NGX_OK) { + ngx_http_upstream_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } } for ( ;; ) { From mdounin at mdounin.ru Fri Aug 8 20:09:02 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:09:02 +0300 Subject: [PATCH 07 of 12] Style In-Reply-To: References: Message-ID: <081c23342274d5725978.1754683742@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1754683256 -10800 # Fri Aug 08 23:00:56 2025 +0300 # Node ID 081c23342274d57259786c115dd55c08eb03ac9e # Parent 924c082b02a4afed6910650d6ee3b709244a0fba Style. 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 @@ -1808,7 +1808,6 @@ ngx_http_parse_status_line(ngx_http_requ switch (ch) { case CR: state = sw_almost_done; - break; case LF: goto done; From mdounin at mdounin.ru Fri Aug 8 20:09:03 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:09:03 +0300 Subject: [PATCH 08 of 12] Reworked ngx_http_parse_status_line() to avoid data assumptions In-Reply-To: References: Message-ID: <9c5a58441dc9fd622806.1754683743@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1754683258 -10800 # Fri Aug 08 23:00:58 2025 +0300 # Node ID 9c5a58441dc9fd62280681e7f92b74b4e69e5104 # Parent 081c23342274d57259786c115dd55c08eb03ac9e Reworked ngx_http_parse_status_line() to avoid data assumptions. With this change, ngx_http_parse_status_line() does not assume anything about ngx_http_status_t initial values, and does not need it to be cleared on allocation or during reinitialization. Further, status->count is no longer used at all, separate states are used instead. Additionally, status digits parsing now does not permit spaces between digits, which previously were allowed, yet resulted in incorrect status line sent to the client. diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c --- a/src/http/modules/ngx_http_proxy_module.c +++ b/src/http/modules/ngx_http_proxy_module.c @@ -1628,10 +1628,6 @@ ngx_http_proxy_reinit_request(ngx_http_r return NGX_OK; } - ctx->status.code = 0; - ctx->status.count = 0; - ctx->status.start = NULL; - ctx->status.end = NULL; ctx->chunked.state = 0; r->upstream->process_header = ngx_http_proxy_process_status_line; diff --git a/src/http/modules/ngx_http_scgi_module.c b/src/http/modules/ngx_http_scgi_module.c --- a/src/http/modules/ngx_http_scgi_module.c +++ b/src/http/modules/ngx_http_scgi_module.c @@ -489,7 +489,7 @@ ngx_http_scgi_handler(ngx_http_request_t return NGX_HTTP_INTERNAL_SERVER_ERROR; } - status = ngx_pcalloc(r->pool, sizeof(ngx_http_status_t)); + status = ngx_palloc(r->pool, sizeof(ngx_http_status_t)); if (status == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } @@ -985,19 +985,6 @@ ngx_http_scgi_create_request(ngx_http_re static ngx_int_t ngx_http_scgi_reinit_request(ngx_http_request_t *r) { - ngx_http_status_t *status; - - status = ngx_http_get_module_ctx(r, ngx_http_scgi_module); - - if (status == NULL) { - return NGX_OK; - } - - status->code = 0; - status->count = 0; - status->start = NULL; - status->end = NULL; - r->upstream->process_header = ngx_http_scgi_process_status_line; r->state = 0; diff --git a/src/http/modules/ngx_http_uwsgi_module.c b/src/http/modules/ngx_http_uwsgi_module.c --- a/src/http/modules/ngx_http_uwsgi_module.c +++ b/src/http/modules/ngx_http_uwsgi_module.c @@ -657,7 +657,7 @@ ngx_http_uwsgi_handler(ngx_http_request_ return NGX_HTTP_INTERNAL_SERVER_ERROR; } - status = ngx_pcalloc(r->pool, sizeof(ngx_http_status_t)); + status = ngx_palloc(r->pool, sizeof(ngx_http_status_t)); if (status == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } @@ -1214,19 +1214,6 @@ ngx_http_uwsgi_create_request(ngx_http_r static ngx_int_t ngx_http_uwsgi_reinit_request(ngx_http_request_t *r) { - ngx_http_status_t *status; - - status = ngx_http_get_module_ctx(r, ngx_http_uwsgi_module); - - if (status == NULL) { - return NGX_OK; - } - - status->code = 0; - status->count = 0; - status->start = NULL; - status->end = NULL; - r->upstream->process_header = ngx_http_uwsgi_process_status_line; r->state = 0; diff --git a/src/http/ngx_http.h b/src/http/ngx_http.h --- a/src/http/ngx_http.h +++ b/src/http/ngx_http.h @@ -72,7 +72,6 @@ struct ngx_http_chunked_s { typedef struct { ngx_uint_t http_version; ngx_uint_t code; - ngx_uint_t count; u_char *start; u_char *end; } ngx_http_status_t; 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 @@ -1645,7 +1645,9 @@ ngx_http_parse_status_line(ngx_http_requ sw_major_digit, sw_first_minor_digit, sw_minor_digit, - sw_status, + sw_first_status_digit, + sw_second_status_digit, + sw_third_status_digit, sw_space_after_status, sw_status_text, sw_almost_done @@ -1750,7 +1752,7 @@ ngx_http_parse_status_line(ngx_http_requ /* the minor HTTP version or the end of the request line */ case sw_minor_digit: if (ch == ' ') { - state = sw_status; + state = sw_first_status_digit; break; } @@ -1765,8 +1767,8 @@ ngx_http_parse_status_line(ngx_http_requ r->http_minor = r->http_minor * 10 + (ch - '0'); break; - /* HTTP status code */ - case sw_status: + /* the first digit of HTTP status code */ + case sw_first_status_digit: if (ch == ' ') { break; } @@ -1775,13 +1777,29 @@ ngx_http_parse_status_line(ngx_http_requ return NGX_ERROR; } + status->code = ch - '0'; + status->start = p; + state = sw_second_status_digit; + break; + + /* the second digit of HTTP status code */ + case sw_second_status_digit: + if (ch < '0' || ch > '9') { + return NGX_ERROR; + } + status->code = status->code * 10 + (ch - '0'); - - if (++status->count == 3) { - state = sw_space_after_status; - status->start = p - 2; + state = sw_third_status_digit; + break; + + /* the third digit of HTTP status code */ + case sw_third_status_digit: + if (ch < '0' || ch > '9') { + return NGX_ERROR; } + status->code = status->code * 10 + (ch - '0'); + state = sw_space_after_status; break; /* space or end of line */ @@ -1810,6 +1828,7 @@ ngx_http_parse_status_line(ngx_http_requ state = sw_almost_done; break; case LF: + status->end = p; goto done; } break; @@ -1835,10 +1854,6 @@ done: b->pos = p + 1; - if (status->end == NULL) { - status->end = p; - } - status->http_version = r->http_major * 1000 + r->http_minor; r->state = sw_start; From mdounin at mdounin.ru Fri Aug 8 20:09:04 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:09:04 +0300 Subject: [PATCH 09 of 12] gRPC: reinitialization of ping and settings limits In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1754683261 -10800 # Fri Aug 08 23:01:01 2025 +0300 # Node ID e7b982e718785a517c34ffee9422ae9db3230e82 # Parent 9c5a58441dc9fd62280681e7f92b74b4e69e5104 gRPC: reinitialization of ping and settings limits. Allocations for PING and SETTINGS frames were limited in 7379:57463f4e2fcd to prevent potential excessive memory usage due to misbehaving upstream servers. The limit as implemented applies to all interactions with all upstream servers within an upstream, that is, if the limit is reached, switching to the next upstream server is likely to hit the limit again on the next PING (or SETTINGS) frame. This is believed to be incorrect: other upstream servers shouldn't be responsible for misbehaviour of the previous one, and only allocations within a particular connection should be limited. Fix is to reset limits on request reinitialization. diff --git a/src/http/modules/ngx_http_grpc_module.c b/src/http/modules/ngx_http_grpc_module.c --- a/src/http/modules/ngx_http_grpc_module.c +++ b/src/http/modules/ngx_http_grpc_module.c @@ -1216,6 +1216,8 @@ ngx_http_grpc_reinit_request(ngx_http_re ctx->rst = 0; ctx->goaway = 0; ctx->connection = NULL; + ctx->pings = 0; + ctx->settings = 0; return NGX_OK; } From mdounin at mdounin.ru Fri Aug 8 20:09:05 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:09:05 +0300 Subject: [PATCH 10 of 12] Upstream: unexpected connection upgrades now rejected In-Reply-To: References: Message-ID: <07b82a889a0e69eef4d9.1754683745@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1754683266 -10800 # Fri Aug 08 23:01:06 2025 +0300 # Node ID 07b82a889a0e69eef4d90c3457b390dbdb38e2c9 # Parent e7b982e718785a517c34ffee9422ae9db3230e82 Upstream: unexpected connection upgrades now rejected. Unless the client explicitly requested to change the application protocol, the "101 Switching Protocols" response is now considered to be an upstream server error and not forwarded to the client. Similarly, other 1xx responses are also rejected for now. This will be changed in subsequent patches. This ensures that such responses won't affect the connection with the client. diff --git a/src/http/modules/ngx_http_fastcgi_module.c b/src/http/modules/ngx_http_fastcgi_module.c --- a/src/http/modules/ngx_http_fastcgi_module.c +++ b/src/http/modules/ngx_http_fastcgi_module.c @@ -2063,6 +2063,19 @@ ngx_http_fastcgi_process_header(ngx_http ngx_str_set(&u->headers_in.status_line, "200 OK"); } + if (u->headers_in.status_n < NGX_HTTP_OK) { + + /* reject 1xx responses */ + + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "upstream sent unexpected status \"%V\"", + u->headers_in.status_line.len + ? &u->headers_in.status_line + : &u->headers_in.status->value); + + return NGX_HTTP_UPSTREAM_INVALID_HEADER; + } + break; } diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c --- a/src/http/modules/ngx_http_proxy_module.c +++ b/src/http/modules/ngx_http_proxy_module.c @@ -2009,12 +2009,21 @@ ngx_http_proxy_process_header(ngx_http_r u->keepalive = !u->headers_in.connection_close; } - if (u->headers_in.status_n == NGX_HTTP_SWITCHING_PROTOCOLS) { + if (u->headers_in.status_n == NGX_HTTP_SWITCHING_PROTOCOLS + && r->headers_in.upgrade) + { u->keepalive = 0; - - if (r->headers_in.upgrade) { - u->upgrade = 1; - } + u->upgrade = 1; + + } else if (u->headers_in.status_n < NGX_HTTP_OK) { + + /* reject unexpected 1xx responses */ + + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "upstream sent unexpected status \"%V\"", + &u->headers_in.status_line); + + return NGX_HTTP_UPSTREAM_INVALID_HEADER; } return NGX_OK; diff --git a/src/http/modules/ngx_http_scgi_module.c b/src/http/modules/ngx_http_scgi_module.c --- a/src/http/modules/ngx_http_scgi_module.c +++ b/src/http/modules/ngx_http_scgi_module.c @@ -1157,6 +1157,18 @@ ngx_http_scgi_process_header(ngx_http_re && r->headers_in.upgrade) { u->upgrade = 1; + + } else if (u->headers_in.status_n < NGX_HTTP_OK) { + + /* reject unexpected 1xx responses */ + + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "upstream sent unexpected status \"%V\"", + u->headers_in.status_line.len + ? &u->headers_in.status_line + : &u->headers_in.status->value); + + return NGX_HTTP_UPSTREAM_INVALID_HEADER; } return NGX_OK; diff --git a/src/http/modules/ngx_http_uwsgi_module.c b/src/http/modules/ngx_http_uwsgi_module.c --- a/src/http/modules/ngx_http_uwsgi_module.c +++ b/src/http/modules/ngx_http_uwsgi_module.c @@ -1386,6 +1386,18 @@ ngx_http_uwsgi_process_header(ngx_http_r && r->headers_in.upgrade) { u->upgrade = 1; + + } else if (u->headers_in.status_n < NGX_HTTP_OK) { + + /* reject unexpected 1xx responses */ + + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "upstream sent unexpected status \"%V\"", + u->headers_in.status_line.len + ? &u->headers_in.status_line + : &u->headers_in.status->value); + + return NGX_HTTP_UPSTREAM_INVALID_HEADER; } return NGX_OK; From mdounin at mdounin.ru Fri Aug 8 20:09:06 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:09:06 +0300 Subject: [PATCH 11 of 12] Proxy: connection upgrades now rejected if not configured In-Reply-To: References: Message-ID: <9b2abd883ed2b44c0b48.1754683746@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1754683269 -10800 # Fri Aug 08 23:01:09 2025 +0300 # Node ID 9b2abd883ed2b44c0b48db7506f24d0a326bfb7f # Parent 07b82a889a0e69eef4d90c3457b390dbdb38e2c9 Proxy: connection upgrades now rejected if not configured. Previously, connection upgrades from upstream servers were accepted as long as they were requested by the client. With this change, we additionally check that the "Upgrade" header was actually sent to the upstream server, as per "proxy_set_header Upgrade ..." in the configuration. This shouldn't change anything for well-behaving upstream servers, though makes things safer to use with misbehaving ones (and assuming the client uses the "Upgrade" header for unrelated reasons, such as when trying to start HTTP/2 over cleartext TCP with "Upgrade: h2c", currently deprecated). diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c --- a/src/http/modules/ngx_http_proxy_module.c +++ b/src/http/modules/ngx_http_proxy_module.c @@ -141,6 +141,7 @@ typedef struct { ngx_chain_t *busy; unsigned head:1; + unsigned upgrade:1; unsigned internal_chunked:1; unsigned header_sent:1; } ngx_http_proxy_ctx_t; @@ -1244,6 +1245,7 @@ ngx_http_proxy_create_key(ngx_http_reque static ngx_int_t ngx_http_proxy_create_request(ngx_http_request_t *r) { + u_char *key; size_t len, uri_len, loc_len, body_len, key_len, val_len; uintptr_t escape; @@ -1498,9 +1500,17 @@ ngx_http_proxy_create_request(ngx_http_r continue; } + key = e.pos; + code = *(ngx_http_script_code_pt *) e.ip; code((ngx_http_script_engine_t *) &e); + if (e.pos - key == 7 + && ngx_strncasecmp(key, (u_char *) "Upgrade", 7) == 0) + { + ctx->upgrade = 1; + } + *e.pos++ = ':'; *e.pos++ = ' '; while (*(uintptr_t *) e.ip) { @@ -2010,7 +2020,8 @@ ngx_http_proxy_process_header(ngx_http_r } if (u->headers_in.status_n == NGX_HTTP_SWITCHING_PROTOCOLS - && r->headers_in.upgrade) + && r->headers_in.upgrade + && ctx->upgrade) { u->keepalive = 0; u->upgrade = 1; From mdounin at mdounin.ru Fri Aug 8 20:09:07 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:09:07 +0300 Subject: [PATCH 12 of 12] Upstream: unexpected 1xx interim responses now ignored In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1754683274 -10800 # Fri Aug 08 23:01:14 2025 +0300 # Node ID f7e18803d4411a20e25a0f7f33bfb7e281cc1739 # Parent 9b2abd883ed2b44c0b48db7506f24d0a326bfb7f Upstream: unexpected 1xx interim responses now ignored. Unexpected 1xx interim informational responses received before the final response are now parsed and ignored. This is required for HTTP/1.1 and above, and expected to provide better interoperability. Notable exception is 101 (Switching Protocols), which is rejected unless requested by the client, as it is expected to be followed by other protocol data which won't be able to parse anyway. Similarly, invalid responses with status codes below 100 are rejected as well. Amount of 1xx responses which can be received from an upstream server and ignored is generally limited by the buffer size. For gRPC, since buffer can be reset between reading HEADERS frames, total number of 1xx responses is additionally limited. The limit is set to 10, which happens to match the limit used by Apache. diff --git a/src/http/modules/ngx_http_fastcgi_module.c b/src/http/modules/ngx_http_fastcgi_module.c --- a/src/http/modules/ngx_http_fastcgi_module.c +++ b/src/http/modules/ngx_http_fastcgi_module.c @@ -2063,10 +2063,9 @@ ngx_http_fastcgi_process_header(ngx_http ngx_str_set(&u->headers_in.status_line, "200 OK"); } - if (u->headers_in.status_n < NGX_HTTP_OK) { - - /* reject 1xx responses */ - + if (u->headers_in.status_n == NGX_HTTP_SWITCHING_PROTOCOLS + || u->headers_in.status_n < NGX_HTTP_CONTINUE) + { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "upstream sent unexpected status \"%V\"", u->headers_in.status_line.len @@ -2074,6 +2073,20 @@ ngx_http_fastcgi_process_header(ngx_http : &u->headers_in.status->value); return NGX_HTTP_UPSTREAM_INVALID_HEADER; + + } else if (u->headers_in.status_n < NGX_HTTP_OK) { + + /* ignore unexpected 1xx responses */ + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http fastcgi 1xx ignored"); + + if (ngx_http_upstream_clear_headers(r, u) != NGX_OK) { + return NGX_ERROR; + } + + rc = NGX_OK; + break; } break; diff --git a/src/http/modules/ngx_http_grpc_module.c b/src/http/modules/ngx_http_grpc_module.c --- a/src/http/modules/ngx_http_grpc_module.c +++ b/src/http/modules/ngx_http_grpc_module.c @@ -82,6 +82,7 @@ typedef struct { ngx_uint_t pings; ngx_uint_t settings; + ngx_uint_t headers; off_t length; @@ -1218,6 +1219,7 @@ ngx_http_grpc_reinit_request(ngx_http_re ctx->connection = NULL; ctx->pings = 0; ctx->settings = 0; + ctx->headers = 0; return NGX_OK; } @@ -1860,13 +1862,6 @@ ngx_http_grpc_process_header(ngx_http_re return NGX_HTTP_UPSTREAM_INVALID_HEADER; } - if (status < NGX_HTTP_OK) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, - "upstream sent unexpected :status \"%V\"", - status_line); - return NGX_HTTP_UPSTREAM_INVALID_HEADER; - } - u->headers_in.status_n = status; ctx->status = 1; @@ -1910,6 +1905,44 @@ ngx_http_grpc_process_header(ngx_http_re ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "grpc header done"); + if (u->headers_in.status_n == NGX_HTTP_SWITCHING_PROTOCOLS + || u->headers_in.status_n < NGX_HTTP_CONTINUE) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "upstream sent unexpected status \"%03ui\"", + u->headers_in.status_n); + + return NGX_HTTP_UPSTREAM_INVALID_HEADER; + + } else if (u->headers_in.status_n < NGX_HTTP_OK) { + + /* ignore unexpected 1xx responses */ + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "grpc 1xx ignored"); + + if (ctx->end_stream) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "upstream sent 1xx response " + "with end stream flag"); + return NGX_HTTP_UPSTREAM_INVALID_HEADER; + } + + if (ctx->headers++ > 10) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "upstream sent too many 1xx responses"); + return NGX_HTTP_UPSTREAM_INVALID_HEADER; + } + + if (ngx_http_upstream_clear_headers(r, u) != NGX_OK) { + return NGX_ERROR; + } + + ctx->status = 0; + + break; + } + if (ctx->end_stream) { u->headers_in.content_length_n = 0; diff --git a/src/http/modules/ngx_http_proxy_module.c b/src/http/modules/ngx_http_proxy_module.c --- a/src/http/modules/ngx_http_proxy_module.c +++ b/src/http/modules/ngx_http_proxy_module.c @@ -2026,15 +2026,30 @@ ngx_http_proxy_process_header(ngx_http_r u->keepalive = 0; u->upgrade = 1; - } else if (u->headers_in.status_n < NGX_HTTP_OK) { - - /* reject unexpected 1xx responses */ - + } else if (u->headers_in.status_n == NGX_HTTP_SWITCHING_PROTOCOLS + || u->headers_in.status_n < NGX_HTTP_CONTINUE) + { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "upstream sent unexpected status \"%V\"", &u->headers_in.status_line); return NGX_HTTP_UPSTREAM_INVALID_HEADER; + + } else if (u->headers_in.status_n < NGX_HTTP_OK) { + + /* ignore unexpected 1xx responses */ + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http proxy 1xx ignored"); + + u->keepalive = 0; + u->process_header = ngx_http_proxy_process_status_line; + + if (ngx_http_upstream_clear_headers(r, u) != NGX_OK) { + return NGX_ERROR; + } + + return ngx_http_proxy_process_status_line(r); } return NGX_OK; diff --git a/src/http/modules/ngx_http_scgi_module.c b/src/http/modules/ngx_http_scgi_module.c --- a/src/http/modules/ngx_http_scgi_module.c +++ b/src/http/modules/ngx_http_scgi_module.c @@ -1158,10 +1158,9 @@ ngx_http_scgi_process_header(ngx_http_re { u->upgrade = 1; - } else if (u->headers_in.status_n < NGX_HTTP_OK) { - - /* reject unexpected 1xx responses */ - + } else if (u->headers_in.status_n == NGX_HTTP_SWITCHING_PROTOCOLS + || u->headers_in.status_n < NGX_HTTP_CONTINUE) + { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "upstream sent unexpected status \"%V\"", u->headers_in.status_line.len @@ -1169,6 +1168,21 @@ ngx_http_scgi_process_header(ngx_http_re : &u->headers_in.status->value); return NGX_HTTP_UPSTREAM_INVALID_HEADER; + + } else if (u->headers_in.status_n < NGX_HTTP_OK) { + + /* ignore unexpected 1xx responses */ + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http scgi 1xx ignored"); + + u->process_header = ngx_http_scgi_process_status_line; + + if (ngx_http_upstream_clear_headers(r, u) != NGX_OK) { + return NGX_ERROR; + } + + return ngx_http_scgi_process_status_line(r); } return NGX_OK; diff --git a/src/http/modules/ngx_http_uwsgi_module.c b/src/http/modules/ngx_http_uwsgi_module.c --- a/src/http/modules/ngx_http_uwsgi_module.c +++ b/src/http/modules/ngx_http_uwsgi_module.c @@ -1387,10 +1387,9 @@ ngx_http_uwsgi_process_header(ngx_http_r { u->upgrade = 1; - } else if (u->headers_in.status_n < NGX_HTTP_OK) { - - /* reject unexpected 1xx responses */ - + } else if (u->headers_in.status_n == NGX_HTTP_SWITCHING_PROTOCOLS + || u->headers_in.status_n < NGX_HTTP_CONTINUE) + { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "upstream sent unexpected status \"%V\"", u->headers_in.status_line.len @@ -1398,6 +1397,21 @@ ngx_http_uwsgi_process_header(ngx_http_r : &u->headers_in.status->value); return NGX_HTTP_UPSTREAM_INVALID_HEADER; + + } else if (u->headers_in.status_n < NGX_HTTP_OK) { + + /* ignore unexpected 1xx responses */ + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http uwsgi 1xx ignored"); + + u->process_header = ngx_http_uwsgi_process_status_line; + + if (ngx_http_upstream_clear_headers(r, u) != NGX_OK) { + return NGX_ERROR; + } + + return ngx_http_uwsgi_process_status_line(r); } return NGX_OK; 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 @@ -45,8 +45,6 @@ static void ngx_http_upstream_connect(ng ngx_http_upstream_t *u); static ngx_int_t ngx_http_upstream_reinit(ngx_http_request_t *r, ngx_http_upstream_t *u); -static ngx_int_t ngx_http_upstream_clear_headers(ngx_http_request_t *r, - ngx_http_upstream_t *u); static void ngx_http_upstream_send_request(ngx_http_request_t *r, ngx_http_upstream_t *u, ngx_uint_t do_write); static ngx_int_t ngx_http_upstream_send_request_body(ngx_http_request_t *r, @@ -2070,7 +2068,7 @@ ngx_http_upstream_reinit(ngx_http_reques } -static ngx_int_t +ngx_int_t ngx_http_upstream_clear_headers(ngx_http_request_t *r, ngx_http_upstream_t *u) { if (u->headers_in.headers.last) { diff --git a/src/http/ngx_http_upstream.h b/src/http/ngx_http_upstream.h --- a/src/http/ngx_http_upstream.h +++ b/src/http/ngx_http_upstream.h @@ -421,6 +421,8 @@ typedef struct { ngx_int_t ngx_http_upstream_create(ngx_http_request_t *r); void ngx_http_upstream_init(ngx_http_request_t *r); +ngx_int_t ngx_http_upstream_clear_headers(ngx_http_request_t *r, + ngx_http_upstream_t *u); ngx_int_t ngx_http_upstream_non_buffered_filter_init(void *data); ngx_int_t ngx_http_upstream_non_buffered_filter(void *data, ssize_t bytes); ngx_http_upstream_srv_conf_t *ngx_http_upstream_add(ngx_conf_t *cf, From mdounin at mdounin.ru Fri Aug 8 20:20:05 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:20:05 +0300 Subject: [PATCH 1 of 2] Tests: removed bytes sent tests on rejected connection upgrades In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1753423860 -10800 # Fri Jul 25 09:11:00 2025 +0300 # Node ID f43f8260fc6802f1193115a92c6dc1d2f23a0f7d # Parent 0b65f4c779066132ebb782923c8f3b3a0c03904a Tests: removed bytes sent tests on rejected connection upgrades. In proxy_upgrade.t and ssl_proxy_upgrade.t, $body_bytes_sent was expected to be 0 if connection upgrade was not requested by the client, yet the upstream server tried to return 101 (Switching Protocols). This is, however, not the only possible valid outcome. In particular, if such a response is considered to be an error, and 502 (Bad Gateway) is generated, $body_bytes_sent won't be 0, breaking the test. As such, these tests were removed. diff --git a/proxy_upgrade.t b/proxy_upgrade.t --- a/proxy_upgrade.t +++ b/proxy_upgrade.t @@ -28,7 +28,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new()->has(qw/http proxy ssi/) - ->write_file_expand('nginx.conf', <<'EOF')->plan(31); + ->write_file_expand('nginx.conf', <<'EOF')->plan(30); %%TEST_GLOBALS%% @@ -149,7 +149,6 @@ open my $f, '<', "$d/cc.log" or die "Can is($f->getline(), shift (@r) . " 540793 upgrade\n", 'log - bytes'); is($f->getline(), shift (@r) . " 22 upgrade\n", 'log - bytes pipelined'); -like($f->getline(), qr/\d+ 0 /, 'log - bytes noupgrade'); ############################################################################### diff --git a/ssl_proxy_upgrade.t b/ssl_proxy_upgrade.t --- a/ssl_proxy_upgrade.t +++ b/ssl_proxy_upgrade.t @@ -31,7 +31,7 @@ select STDOUT; $| = 1; my $t = Test::Nginx->new()->has(qw/http proxy http_ssl socket_ssl/) ->has_daemon('openssl') - ->write_file_expand('nginx.conf', <<'EOF')->plan(30); + ->write_file_expand('nginx.conf', <<'EOF')->plan(29); %%TEST_GLOBALS%% @@ -160,7 +160,6 @@ open my $f, '<', "$d/cc.log" or die "Can is($f->getline(), shift (@r) . " 540793\n", 'log - bytes'); is($f->getline(), shift (@r) . " 22\n", 'log - bytes pipelined'); -like($f->getline(), qr/\d+ 0\n/, 'log - bytes noupgrade'); ############################################################################### From mdounin at mdounin.ru Fri Aug 8 20:20:06 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 08 Aug 2025 23:20:06 +0300 Subject: [PATCH 2 of 2] Tests: upstream status tests In-Reply-To: References: Message-ID: <393cb49bff677e001e47.1754684406@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1754683511 -10800 # Fri Aug 08 23:05:11 2025 +0300 # Node ID 393cb49bff677e001e47141089d0c7df7f5eb6e1 # Parent f43f8260fc6802f1193115a92c6dc1d2f23a0f7d Tests: upstream status tests. diff --git a/fastcgi_status.t b/fastcgi_status.t new file mode 100644 --- /dev/null +++ b/fastcgi_status.t @@ -0,0 +1,146 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Test for fastcgi backend returning various status codes. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +eval { require FCGI; }; +plan(skip_all => 'FCGI not installed') if $@; +plan(skip_all => 'win32') if $^O eq 'MSWin32'; + +my $t = Test::Nginx->new() + ->has(qw/http fastcgi/)->plan(11) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + fastcgi_param REQUEST_URI $request_uri; + fastcgi_param REQUEST_METHOD $request_method; + + fastcgi_read_timeout 10s; + add_header X-Upstream-Status $upstream_status; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + fastcgi_pass 127.0.0.1:8081; + } + } +} + +EOF + +$t->run_daemon(\&fastcgi_daemon); +$t->run()->waitforsocket('127.0.0.1:' . port(8081)); + +############################################################################### + +like(http_get('/'), qr!^HTTP/1.1 200 !s, 'status 200'); +like(http_get('/600'), qr!^HTTP/1.1 600 !s, 'status 600 non-standard'); + +like(http_get('/status-line'), qr!^HTTP/1.1 200 !s, 'status line ignored'); +like(http_get('/status-no-text'), qr!^HTTP/1.1 204 !s, 'status header no text'); + +like(http_get('/no-status'), qr!^HTTP/1.1 200 !s, 'default status'); +like(http_get('/no-status-location'), qr!^HTTP/1.1 302 !s, + 'default status with location'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +# 1xx responses are ignored since 1.29.1, and 101 (Switching Protocols) +# is rejected + +like(http_get('/100'), qr!^HTTP/1.1 200 .*X-Upstream-Status: 200!s, + 'status 100 ignored'); +like(http_get('/103'), qr!^HTTP/1.1 200 !s, 'status 103 ignored'); + +like(http_get('/101'), qr!^HTTP/1.1 502 !s, 'status 101 rejected'); +like(http_get('/101-no-text'), qr!^HTTP/1.1 502 !s, 'status 101 rejected'); + +like(http_get('/001'), qr!^HTTP/1.1 502 !s, 'status 001 rejected'); + +} + +############################################################################### + +sub fastcgi_daemon { + my $socket = FCGI::OpenSocket('127.0.0.1:' . port(8081), 5); + my $request = FCGI::Request(\*STDIN, \*STDOUT, \*STDERR, \%ENV, + $socket); + + my ($uri, $head); + + while( $request->Accept() >= 0 ) { + $uri = $ENV{REQUEST_URI}; + + if ($uri eq '/') { + print "Status: 200 OK\n\n"; + + } elsif ($uri eq '/600') { + print "Status: 600 Non-standard\n\n"; + + } elsif ($uri eq '/status-line') { + print "HTTP/1.0 204 No content\n\n"; + + } elsif ($uri eq '/status-no-text') { + print "Status: 204\n\n"; + + } elsif ($uri eq '/no-status') { + print "Content-Type: text/html\n\n"; + + } elsif ($uri eq '/no-status-location') { + print "Location: /foobar\n\n"; + + } elsif ($uri eq '/100') { + print "Status: 100 Continue\n\n"; + print "Status: 200 OK\n\n"; + + } elsif ($uri eq '/103') { + print "Status: 103 Early Hints\n"; + print "Link: \n\n"; + print "Status: 200 OK\n\n"; + + } elsif ($uri eq '/101') { + print "Status: 101 Switching Protocols\n\n"; + + } elsif ($uri eq '/101-no-text') { + print "Status: 101\n\n"; + + } elsif ($uri eq '/001') { + print "Status: 001 Invalid\n\n"; + print "Status: 200 OK\n\n"; + } + } + + FCGI::CloseSocket($socket); +} + +############################################################################### diff --git a/grpc_status.t b/grpc_status.t new file mode 100644 --- /dev/null +++ b/grpc_status.t @@ -0,0 +1,199 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Tests for grpc backend returning various status codes. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 grpc/)->plan(12); + +$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; + + grpc_read_timeout 10s; + add_header X-Upstream-Status $upstream_status; + + location / { + grpc_pass grpc://127.0.0.1:8081; + } + } +} + +EOF + +$t->run_daemon(\&grpc_daemon); +$t->run()->waitforsocket('127.0.0.1:' . port(8081)); + +############################################################################### + +like(http_get('/'), qr!^HTTP/1.1 200 !s, 'status 200'); +like(http_get('/600'), qr!^HTTP/1.1 600 !s, 'status 600 non-standard'); + +like(http_get('/no-status'), qr!^HTTP/1.1 502 !s, 'no status rejected'); +like(http_get('/duplicate'), qr!^HTTP/1.1 502 !s, 'duplicate status rejected'); +like(http_get('/spaces'), qr!^HTTP/1.1 502 !s, 'status with spaces rejected'); +like(http_get('/nonfirst'), qr!^HTTP/1.1 502 !s, 'non first status rejected'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +# 1xx responses are ignored since 1.29.1 + +like(http_get('/100'), qr!^HTTP/1.1 200 .*X-Upstream-Status: 200!s, + 'status 100 ignored'); +like(http_get('/103'), qr!^HTTP/1.1 200 !s, 'status 103 ignored'); + +} + +like(http_get('/100-end-stream'), qr!^HTTP/1.1 502 !s, + 'status 100 with end stream rejected'); +like(http_get('/100-many'), qr!^HTTP/1.1 502 !s, + 'status 100 many times rejected'); + +like(http_get('/101'), qr!^HTTP/1.1 502 !s, 'status 101 rejected'); +like(http_get('/001'), qr!^HTTP/1.1 502 !s, 'status 001 rejected'); + +############################################################################### + +sub grpc_daemon { + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1:' . port(8081), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + while (my $client = $server->accept()) { + $client->autoflush(1); + + # preface + $client->sysread(my $buf, 24) == 24 + or next; + + my $c = Test::Nginx::HTTP2->new( + 1, socket => $client, pure => 1, preface => "" + ) + or next; + + my $frames = $c->read(all => [{ fin => 4 }]); + my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + + my $sid = $frame->{sid}; + my $uri = $frame->{headers}{':path'}; + my $status; + + if ($uri eq '/') { + $c->new_stream({ headers => [ + { name => ':status', value => '200' }, + ]}, $sid); + + } elsif ($uri eq '/600') { + $c->new_stream({ headers => [ + { name => ':status', value => '600' }, + ]}, $sid); + + } elsif ($uri eq '/no-status') { + $c->new_stream({ headers => [ + { name => 'foo', value => 'bar' }, + ]}, $sid); + + } elsif ($uri eq '/duplicate') { + $c->new_stream({ headers => [ + { name => ':status', value => '200' }, + { name => ':status', value => '204' }, + ]}, $sid); + + } elsif ($uri eq '/spaces') { + $c->new_stream({ headers => [ + { name => ':status', value => '2 0 0' }, + ]}, $sid); + + } elsif ($uri eq '/nonfirst') { + $c->new_stream({ headers => [ + { name => 'foo', value => 'bar' }, + { name => ':status', value => '200' }, + ]}, $sid); + + } elsif ($uri eq '/100') { + $c->new_stream({ body_more => 1, headers => [ + { name => ':status', value => '100' }, + ]}, $sid); + $c->new_stream({ headers => [ + { name => ':status', value => '200' }, + ]}, $sid); + + } elsif ($uri eq '/100-end-stream') { + $c->new_stream({ headers => [ + { name => ':status', value => '100' }, + ]}, $sid); + $c->new_stream({ headers => [ + { name => ':status', value => '200' }, + ]}, $sid); + + } elsif ($uri eq '/100-many') { + $c->new_stream({ body_more => 1, headers => [ + { name => ':status', value => '100' }, + ]}, $sid) + for 1..15; + $c->new_stream({ headers => [ + { name => ':status', value => '200' }, + ]}, $sid); + + } elsif ($uri eq '/103') { + $c->new_stream({ body_more => 1, headers => [ + { name => ':status', value => '103' }, + ]}, $sid); + $c->new_stream({ headers => [ + { name => ':status', value => '200' }, + ]}, $sid); + + } elsif ($uri eq '/101') { + $c->new_stream({ body => 'foo', headers => [ + { name => ':status', value => '101' }, + ]}, $sid); + + } elsif ($uri eq '/001') { + $c->new_stream({ body => 'foo', headers => [ + { name => ':status', value => '101' }, + ]}, $sid); + } + + close $client; + } +} + +############################################################################### diff --git a/proxy_status.t b/proxy_status.t new file mode 100644 --- /dev/null +++ b/proxy_status.t @@ -0,0 +1,190 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Test for http backend returning various status codes. + +############################################################################### + +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 proxy/); + +$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; + + proxy_read_timeout 10s; + add_header X-Upstream-Status $upstream_status; + + location / { + proxy_pass http://127.0.0.1:8081; + } + + location /allow09/ { + proxy_pass http://127.0.0.1:8081/; + proxy_allow_http09 on; + } + } +} + +EOF + +$t->run_daemon(\&http_daemon); +$t->try_run('no proxy_allow_http09')->plan(12); +$t->waitforsocket('127.0.0.1:' . port(8081)); + +############################################################################### + +like(http_get('/'), qr!^HTTP/1.1 200 !s, 'status 200'); +like(http_get('/600'), qr!^HTTP/1.1 600 !s, 'status 600 non-standard'); + +like(http_get('/http10'), qr!^HTTP/1.1 200 !s, 'http 1.0 200'); +like(http_get('/duplicate'), qr!^HTTP/1.1 200 !s, 'duplicate status ignored'); + +# HTTP/0.9 is disabled by default since 1.29.1 + +like(http_get('/http09'), qr!^HTTP/1.1 502 !s, 'http 0.9'); +like(http_get('/allow09/http09'), qr!^HTTP/1.1 200 .*HTTP/0.9!s, + 'http 0.9 allowed'); + +# spaces between digits not allowed since 1.29.1 + +like(http_get('/spaces'), qr!^HTTP/1.1 502 !s, 'status with spaces rejected'); +like(http_get('/allow09/spaces'), qr!^HTTP/1.1 200 OK.*2 0 0 OK!s, + 'status with spaces as http 0.9'); + +# 1xx responses are ignored since 1.29.1, and 101 (Switching Protocols) +# is rejected unless requested by the client and configured + +like(http_get('/100'), qr!^HTTP/1.1 200 .*X-Upstream-Status: 200!s, + 'status 100 ignored'); +like(http_get('/103'), qr!^HTTP/1.1 200 !s, 'status 103 ignored'); + +like(http_get('/101'), qr!^HTTP/1.1 502 !s, 'status 101 rejected'); + +like(http_get('/001'), qr!^HTTP/1.1 502 !s, 'status 001 rejected'); + +############################################################################### + +sub http_daemon { + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => '127.0.0.1:' . port(8081), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + local $SIG{PIPE} = 'IGNORE'; + + while (my $client = $server->accept()) { + $client->autoflush(1); + + my $headers = ''; + my $uri = ''; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + + if ($uri eq '/') { + + print $client + 'HTTP/1.1 200 OK' . CRLF . + 'Connection: close' . CRLF . CRLF; + + } elsif ($uri eq '/600') { + + print $client + 'HTTP/1.1 600 Non-standard' . CRLF . + 'Connection: close' . CRLF . CRLF; + + } elsif ($uri eq '/http10') { + + print $client + 'HTTP/1.0 200 OK' . CRLF . + 'Connection: close' . CRLF . CRLF; + + } elsif ($uri eq '/duplicate') { + + print $client + 'HTTP/1.1 200 OK' . CRLF . + 'HTTP/1.1 204 No content' . CRLF . + 'Connection: close' . CRLF . CRLF; + + } elsif ($uri =~ m!/http09!) { + + print $client 'It is HTTP/0.9 response' . CRLF; + + } elsif ($uri =~ m!/spaces!) { + + print $client + 'HTTP/1.1 2 0 0 OK' . CRLF . + 'Connection: close' . CRLF . CRLF; + + } elsif ($uri eq '/100') { + + print $client + 'HTTP/1.1 100 Continue' . CRLF . CRLF . + 'HTTP/1.1 200 OK' . CRLF . + 'Connection: close' . CRLF . CRLF; + + } elsif ($uri eq '/103') { + + print $client + 'HTTP/1.1 103 Early Hints' . CRLF . + 'Link: ' . CRLF . CRLF . + 'HTTP/1.1 200 OK' . CRLF . + 'Connection: close' . CRLF . CRLF; + + } elsif ($uri eq '/101') { + + print $client + 'HTTP/1.1 101 Switching' . CRLF . CRLF . + 'HTTP/1.1 200 OK' . CRLF . + 'Connection: close' . CRLF . CRLF; + + } elsif ($uri eq '/001') { + + print $client + 'HTTP/1.1 001 Invalid' . CRLF . CRLF . + 'HTTP/1.1 200 OK' . CRLF . + 'Connection: close' . CRLF . CRLF; + + } + + close $client; + } +} + +############################################################################### diff --git a/scgi_status.t b/scgi_status.t new file mode 100644 --- /dev/null +++ b/scgi_status.t @@ -0,0 +1,153 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Test for scgi backend returning various status codes. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +eval { require SCGI; }; +plan(skip_all => 'SCGI not installed') if $@; + +my $t = Test::Nginx->new() + ->has(qw/http scgi/)->plan(11) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + scgi_param SCGI 1; + scgi_param REQUEST_URI $request_uri; + scgi_param REQUEST_METHOD $request_method; + + scgi_read_timeout 10s; + add_header X-Upstream-Status $upstream_status; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + scgi_pass 127.0.0.1:8081; + } + } +} + +EOF + +$t->run_daemon(\&scgi_daemon); +$t->run()->waitforsocket('127.0.0.1:' . port(8081)); + +############################################################################### + +like(http_get('/'), qr!^HTTP/1.1 200 !s, 'status 200'); +like(http_get('/600'), qr!^HTTP/1.1 600 !s, 'status 600 non-standard'); + +like(http_get('/status-line'), qr!^HTTP/1.1 204 !s, 'status line'); +like(http_get('/status-no-text'), qr!^HTTP/1.1 204 !s, 'status header no text'); + +like(http_get('/no-status'), qr!^HTTP/1.1 200 !s, 'default status'); +like(http_get('/no-status-location'), qr!^HTTP/1.1 302 !s, + 'default status with location'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +# 1xx responses are ignored since 1.29.1, and 101 (Switching Protocols) +# is rejected unless requested by the client + +like(http_get('/100'), qr!^HTTP/1.1 200 .*X-Upstream-Status: 200!s, + 'status 100 ignored'); +like(http_get('/103'), qr!^HTTP/1.1 200 !s, 'status 103 ignored'); + +like(http_get('/101'), qr!^HTTP/1.1 502 !s, 'status 101 rejected'); +like(http_get('/101-no-text'), qr!^HTTP/1.1 502 !s, 'status 101 rejected'); + +like(http_get('/001'), qr!^HTTP/1.1 502 !s, 'status 001 rejected'); + +} + +############################################################################### + +sub scgi_daemon { + my $server = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1:' . port(8081), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $scgi = SCGI->new($server, blocking => 1); + my ($c, $uri); + + while (my $request = $scgi->accept()) { + eval { $request->read_env(); }; + next if $@; + + $uri = $request->env->{REQUEST_URI}; + + $c = $request->connection(); + + if ($uri eq '/') { + $c->print("Status: 200 OK\n\n"); + + } elsif ($uri eq '/600') { + $c->print("Status: 600 Non-standard\n\n"); + + } elsif ($uri eq '/status-line') { + $c->print("HTTP/1.0 204 No content\n\n"); + + } elsif ($uri eq '/status-no-text') { + $c->print("Status: 204\n\n"); + + } elsif ($uri eq '/no-status') { + $c->print("Content-Type: text/html\n\n"); + + } elsif ($uri eq '/no-status-location') { + $c->print("Location: /foobar\n\n"); + + } elsif ($uri eq '/100') { + $c->print("Status: 100 Continue\n\n"); + $c->print("Status: 200 OK\n\n"); + + } elsif ($uri eq '/103') { + $c->print("Status: 103 Early Hints\n"); + $c->print("Link: \n\n"); + $c->print("Status: 200 OK\n\n"); + + } elsif ($uri eq '/101') { + $c->print("Status: 101 Switching Protocols\n\n"); + + } elsif ($uri eq '/101-no-text') { + $c->print("Status: 101\n\n"); + + } elsif ($uri eq '/001') { + $c->print("Status: 001 Invalid\n\n"); + } + } +} + +############################################################################### From mdounin at mdounin.ru Thu Aug 14 15:52:01 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Thu, 14 Aug 2025 18:52:01 +0300 Subject: [PATCH 1 of 2] SSL: fixed subjectAltName and commonName debug logging Message-ID: <51d23ff6f109765f4c38.1755186721@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1755133569 -10800 # Thu Aug 14 04:06:09 2025 +0300 # Node ID 51d23ff6f109765f4c382d449d7ea0cea13ea220 # Parent f7e18803d4411a20e25a0f7f33bfb7e281cc1739 SSL: fixed subjectAltName and commonName debug logging. Previously, ASN1_STRING_length() was used as a length for "%*s" format specifier, which is wrong, since string length is expected to be "size_t", and ASN1_STRING_length() returns "int". 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 @@ -4957,7 +4957,8 @@ ngx_ssl_check_host(ngx_connection_t *c, ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL subjectAltName: \"%*s\"", - ASN1_STRING_length(str), ASN1_STRING_data(str)); + (size_t) ASN1_STRING_length(str), + ASN1_STRING_data(str)); if (ngx_ssl_check_name(name, str) == NGX_OK) { ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, @@ -4999,7 +5000,8 @@ ngx_ssl_check_host(ngx_connection_t *c, ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, "SSL commonName: \"%*s\"", - ASN1_STRING_length(str), ASN1_STRING_data(str)); + (size_t) ASN1_STRING_length(str), + ASN1_STRING_data(str)); if (ngx_ssl_check_name(name, str) == NGX_OK) { ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, From mdounin at mdounin.ru Thu Aug 14 15:52:02 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Thu, 14 Aug 2025 18:52:02 +0300 Subject: [PATCH 2 of 2] SSL: support for iPAddress subjectAltName in certificates In-Reply-To: <51d23ff6f109765f4c38.1755186721@vm-bsd.mdounin.ru> References: <51d23ff6f109765f4c38.1755186721@vm-bsd.mdounin.ru> Message-ID: <870dfc16d381d90644b0.1755186722@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1755133582 -10800 # Thu Aug 14 04:06:22 2025 +0300 # Node ID 870dfc16d381d90644b08159dd84d1ee391cf630 # Parent 51d23ff6f109765f4c382d449d7ea0cea13ea220 SSL: support for iPAddress subjectAltName in certificates. Known public services with iPAddress subject altnames include 1.1.1.1 and 8.8.8.8. IP address certificates from Let's Encrypt are expected to be available soon: https://letsencrypt.org/2025/07/01/issuing-our-first-ip-address-certificate 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 @@ -4901,19 +4901,63 @@ ngx_ssl_cleanup_ctx(void *data) ngx_int_t ngx_ssl_check_host(ngx_connection_t *c, ngx_str_t *name) { - X509 *cert; + X509 *cert; + u_char *addr, addr6[16]; + size_t alen; + in_addr_t addr4; cert = SSL_get_peer_certificate(c->ssl->connection); if (cert == NULL) { return NGX_ERROR; } + if (name->len == 0) { + goto failed; + } + + addr4 = ngx_inet_addr(name->data, name->len); + + if (addr4 != INADDR_NONE) { + addr = (u_char *) &addr4; + alen = 4; + +#if (NGX_HAVE_INET6) + } else if (name->data[0] == '[') { + + if (name->data[name->len - 1] != ']') { + goto failed; + } + + if (ngx_inet6_addr(name->data + 1, name->len - 2, &addr6[0]) + != NGX_OK) + { + goto failed; + } + + addr = &addr6[0]; + alen = 16; + +#endif + } else { + addr = NULL; + alen = 0; + } + + #ifdef X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT /* X509_check_host() is only available in OpenSSL 1.0.2+ */ - if (name->len == 0) { - goto failed; + if (addr) { + if (X509_check_ip(cert, addr, alen, 0) != 1) { + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, + "X509_check_ip(): no match"); + goto failed; + } + + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, + "X509_check_ip(): match"); + goto found; } if (X509_check_host(cert, (char *) name->data, name->len, 0, NULL) != 1) { @@ -4924,12 +4968,13 @@ ngx_ssl_check_host(ngx_connection_t *c, ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, "X509_check_host(): match"); - goto found; #else { int n, i; + size_t dlen; + u_char *data; X509_NAME *sname; ASN1_STRING *str; X509_NAME_ENTRY *entry; @@ -4949,22 +4994,63 @@ ngx_ssl_check_host(ngx_connection_t *c, for (i = 0; i < n; i++) { altname = sk_GENERAL_NAME_value(altnames, i); - if (altname->type != GEN_DNS) { - continue; - } - - str = altname->d.dNSName; - - ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, - "SSL subjectAltName: \"%*s\"", - (size_t) ASN1_STRING_length(str), - ASN1_STRING_data(str)); - - if (ngx_ssl_check_name(name, str) == NGX_OK) { - ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, - "SSL subjectAltName: match"); - GENERAL_NAMES_free(altnames); - goto found; + if (altname->type == GEN_IPADD) { + + str = altname->d.iPAddress; + data = ASN1_STRING_data(str); + dlen = ASN1_STRING_length(str); + +#if (NGX_DEBUG) + { + size_t al; + u_char at[NGX_INET6_ADDRSTRLEN]; + + if (dlen == 4) { + al = ngx_inet_ntop(AF_INET, data, at, + NGX_INET6_ADDRSTRLEN); + +#if (NGX_HAVE_INET6) + } else if (dlen == 16) { + al = ngx_inet_ntop(AF_INET6, data, at, + NGX_INET6_ADDRSTRLEN); + +#endif + } else { + al = ngx_cpymem(at, "", sizeof("") - 1) + - at; + } + + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, + "SSL subjectAltName: %*s", + al, at); + } +#endif + + if (addr + && alen == dlen + && ngx_memcmp(addr, data, dlen) == 0) + { + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, + "SSL subjectAltName: match"); + GENERAL_NAMES_free(altnames); + goto found; + } + + } else if (altname->type == GEN_DNS) { + + str = altname->d.dNSName; + + ngx_log_debug2(NGX_LOG_DEBUG_EVENT, c->log, 0, + "SSL subjectAltName: \"%*s\"", + (size_t) ASN1_STRING_length(str), + ASN1_STRING_data(str)); + + if (ngx_ssl_check_name(name, str) == NGX_OK) { + ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0, + "SSL subjectAltName: match"); + GENERAL_NAMES_free(altnames); + goto found; + } } } From mdounin at mdounin.ru Thu Aug 14 15:54:33 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Thu, 14 Aug 2025 18:54:33 +0300 Subject: [PATCH] Tests: iPAddress subjectAltName tests In-Reply-To: <870dfc16d381d90644b0.1755186722@vm-bsd.mdounin.ru> References: <870dfc16d381d90644b0.1755186722@vm-bsd.mdounin.ru> Message-ID: <9d83bf92a409a4430af9.1755186873@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1755186657 -10800 # Thu Aug 14 18:50:57 2025 +0300 # Node ID 9d83bf92a409a4430af9419845d8bcd5461b9c5f # Parent 393cb49bff677e001e47141089d0c7df7f5eb6e1 Tests: iPAddress subjectAltName tests. diff --git a/proxy_ssl_verify.t b/proxy_ssl_verify.t --- a/proxy_ssl_verify.t +++ b/proxy_ssl_verify.t @@ -23,7 +23,7 @@ select STDERR; $| = 1; select STDOUT; $| = 1; my $t = Test::Nginx->new()->has(qw/http http_ssl proxy/) - ->has_daemon('openssl')->plan(6) + ->has_daemon('openssl')->plan(10) ->write_file_expand('nginx.conf', <<'EOF'); %%TEST_GLOBALS%% @@ -81,6 +81,33 @@ http { proxy_ssl_trusted_certificate 1.example.com.crt; proxy_ssl_session_reuse off; } + + location /ip { + proxy_pass https://127.0.0.1:8081/; + proxy_ssl_verify on; + proxy_ssl_trusted_certificate 1.example.com.crt; + } + + location /ip/fail { + proxy_pass https://127.0.0.1:8081/; + proxy_ssl_name 127.0.0.2; + proxy_ssl_verify on; + proxy_ssl_trusted_certificate 1.example.com.crt; + } + + location /ip6 { + proxy_pass https://127.0.0.1:8081/; + proxy_ssl_name [::1]; + proxy_ssl_verify on; + proxy_ssl_trusted_certificate 1.example.com.crt; + } + + location /ip6/fail { + proxy_pass https://127.0.0.1:8081/; + proxy_ssl_name [::2]; + proxy_ssl_verify on; + proxy_ssl_trusted_certificate 1.example.com.crt; + } } server { @@ -118,7 +145,7 @@ x509_extensions = v3_req commonName=no.match.example.com [ v3_req ] -subjectAltName = DNS:example.com,DNS:*.example.com +subjectAltName = DNS:example.com,DNS:*.example.com,IP:127.0.0.1,IP:::1 EOF $t->write_file('openssl.2.example.com.conf', <has_version('1.29.1'); + +like(http_get('/ip'), qr/200 OK/ms, 'verify ipv4'); +like(http_get('/ip6'), qr/200 OK/ms, 'verify ipv6'); + +} + +like(http_get('/ip/fail'), qr/502 Bad/ms, 'verify ipv4 fail'); +like(http_get('/ip6/fail'), qr/502 Bad/ms, 'verify ipv6 fail'); + ############################################################################### From mdounin at mdounin.ru Thu Aug 14 21:01:01 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 15 Aug 2025 00:01:01 +0300 Subject: [PATCH 1 of 2] Mail: s->login and s->passwd now cleared on errors Message-ID: <13ee0b2ffee6d852bc5a.1755205261@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1755187422 -10800 # Thu Aug 14 19:03:42 2025 +0300 # Node ID 13ee0b2ffee6d852bc5aa4e10fcf217a982c8e79 # Parent 870dfc16d381d90644b08159dd84d1ee391cf630 Mail: s->login and s->passwd now cleared on errors. This ensures that rejected logins won't be used, such as in logs. Further, this fixes using uninitialized memory in logs when an error is detected in the middle of an auth mechanism parsing, with s->login partially set, as well as sending uninitialized memory to auth_http server with "auth_smtp none;" (known as CVE-2025-53859, though security impact of this issue is questionable). 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 @@ -986,6 +986,8 @@ ngx_mail_auth_send_error(ngx_mail_sessio s->state = 0; s->mail_state = 0; s->tag.len = 0; + s->login.len = 0; + s->passwd.len = 0; } else { s->auth_err.len -= s->tag.len; 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 @@ -776,6 +776,8 @@ ngx_mail_auth_xoauth2(ngx_mail_session_t s->quit = s->auth_quit; s->state = 0; s->mail_state = 0; + s->login.len = 0; + s->passwd.len = 0; ngx_str_null(&s->auth_err); return NGX_OK; } @@ -885,6 +887,8 @@ ngx_mail_auth_oauthbearer(ngx_mail_sessi s->quit = s->auth_quit; s->state = 0; s->mail_state = 0; + s->login.len = 0; + s->passwd.len = 0; ngx_str_null(&s->auth_err); return NGX_OK; } 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 @@ -251,9 +251,11 @@ ngx_mail_imap_auth_state(ngx_event_t *re return; case NGX_MAIL_PARSE_INVALID_COMMAND: + s->mail_state = ngx_imap_start; s->state = 0; + s->login.len = 0; + s->passwd.len = 0; ngx_str_set(&s->out, imap_invalid_command); - s->mail_state = ngx_imap_start; break; } 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 @@ -290,6 +290,8 @@ ngx_mail_pop3_auth_state(ngx_event_t *re case NGX_MAIL_PARSE_INVALID_COMMAND: s->mail_state = ngx_pop3_start; s->state = 0; + s->login.len = 0; + s->passwd.len = 0; ngx_str_set(&s->out, pop3_invalid_command); 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 @@ -577,6 +577,8 @@ ngx_mail_smtp_auth_state(ngx_event_t *re case NGX_MAIL_PARSE_INVALID_COMMAND: s->mail_state = ngx_smtp_start; s->state = 0; + s->login.len = 0; + s->passwd.len = 0; ngx_str_set(&s->out, smtp_invalid_command); /* fall through */ From mdounin at mdounin.ru Thu Aug 14 21:01:02 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Fri, 15 Aug 2025 00:01:02 +0300 Subject: [PATCH 2 of 2] Mail: fixed "upstream: ..." in logs with "smtp_auth none" In-Reply-To: <13ee0b2ffee6d852bc5a.1755205261@vm-bsd.mdounin.ru> References: <13ee0b2ffee6d852bc5a.1755205261@vm-bsd.mdounin.ru> Message-ID: <31ecd4a1b346b117e841.1755205262@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1755187434 -10800 # Thu Aug 14 19:03:54 2025 +0300 # Node ID 31ecd4a1b346b117e84128988bd7bf056e7aa3b9 # Parent 13ee0b2ffee6d852bc5aa4e10fcf217a982c8e79 Mail: fixed "upstream: ..." in logs with "smtp_auth none". Previously, it was not added to the log line due to no s->login, which caused early return from the function. Fix is to restructure the log handler to use "if (...)" blocks instead, similarly to ngx_http_log_error_handler(). 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 @@ -1486,19 +1486,15 @@ ngx_mail_log_error(ngx_log_t *log, u_cha len -= p - buf; buf = p; - if (s->login.len == 0) { - return p; + if (s->login.len) { + p = ngx_snprintf(buf, len, ", login: \"%V\"", &s->login); + len -= p - buf; + buf = p; } - p = ngx_snprintf(buf, len, ", login: \"%V\"", &s->login); - len -= p - buf; - buf = p; - - if (s->proxy == NULL) { - return p; + if (s->proxy) { + p = ngx_snprintf(buf, len, ", upstream: %V", s->proxy->upstream.name); } - p = ngx_snprintf(buf, len, ", upstream: %V", s->proxy->upstream.name); - return p; }