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; } From becasov at yandex.ru Sun Aug 17 21:46:06 2025 From: becasov at yandex.ru (=?utf-8?B?0JHQtdC60LDRgdC+0LIg0JPQtdC90L3QsNC00LjQuQ==?=) Date: Mon, 18 Aug 2025 00:46:06 +0300 Subject: [PATCH] Added the "proxy_upstream_allow_duplicate_chunked" configuration directive Message-ID: <94401755466911@mail.yandex.ru> An HTML attachment was scrubbed... URL: From mdounin at mdounin.ru Sun Aug 17 23:33:43 2025 From: mdounin at mdounin.ru (Maxim Dounin) Date: Mon, 18 Aug 2025 02:33:43 +0300 Subject: [PATCH 08 of 12] Reworked ngx_http_parse_status_line() to avoid data assumptions In-Reply-To: <9c5a58441dc9fd622806.1754683743@vm-bsd.mdounin.ru> References: <9c5a58441dc9fd622806.1754683743@vm-bsd.mdounin.ru> Message-ID: Hello! On Fri, Aug 08, 2025 at 11:09:03PM +0300, Maxim Dounin wrote: > # 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; > > And yet another "status->end" change, missed in the previous patch: @@ -1797,6 +1815,7 @@ ngx_http_parse_status_line(ngx_http_requ state = sw_almost_done; break; case LF: + status->end = p; goto done; default: return NGX_ERROR; And corresponding test: diff --git a/proxy_status.t b/proxy_status.t --- a/proxy_status.t +++ b/proxy_status.t @@ -57,7 +57,7 @@ http { EOF $t->run_daemon(\&http_daemon); -$t->try_run('no proxy_allow_http09')->plan(12); +$t->try_run('no proxy_allow_http09')->plan(13); $t->waitforsocket('127.0.0.1:' . port(8081)); ############################################################################### $ hg qref $ hg qdiff diff --git a/proxy_status.t b/proxy_status.t --- a/proxy_status.t +++ b/proxy_status.t @@ -10,7 +10,7 @@ use warnings; use strict; use Test::More; -use Socket qw/ CRLF /; +use Socket qw/ CRLF LF /; BEGIN { use FindBin; chdir($FindBin::Bin); } @@ -57,7 +57,7 @@ http { EOF $t->run_daemon(\&http_daemon); -$t->try_run('no proxy_allow_http09')->plan(12); +$t->try_run('no proxy_allow_http09')->plan(13); $t->waitforsocket('127.0.0.1:' . port(8081)); ############################################################################### @@ -68,6 +68,11 @@ like(http_get('/600'), qr!^HTTP/1.1 600 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'); +# status line without text and trailing space, +# invalid but currently accepted + +like(http_get('/notext'), qr!^HTTP/1.1 200!s, 'status without text'); + # HTTP/0.9 is disabled by default since 1.29.1 like(http_get('/http09'), qr!^HTTP/1.1 502 !s, 'http 0.9'); @@ -142,6 +147,12 @@ sub http_daemon { 'HTTP/1.1 204 No content' . CRLF . 'Connection: close' . CRLF . CRLF; + } elsif ($uri eq '/notext') { + + print $client + 'HTTP/1.1 200' . LF . + 'Connection: close' . CRLF . CRLF; + } elsif ($uri =~ m!/http09!) { print $client 'It is HTTP/0.9 response' . CRLF; -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Mon Aug 18 00:30:23 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:30:23 +0300 Subject: [nginx] Proxy: replaced some r->upstream usage with the "u" vari... Message-ID: details: http://freenginx.org/hg/nginx/rev/639c6a28cb0b branches: changeset: 9398:639c6a28cb0b user: Maxim Dounin date: Mon Aug 18 03:18:10 2025 +0300 description: Proxy: replaced some r->upstream usage with the "u" variable. No functional changes. diffstat: src/http/modules/ngx_http_proxy_module.c | 12 ++++++------ 1 files changed, 6 insertions(+), 6 deletions(-) diffs (41 lines): 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 Mon Aug 18 00:30:23 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:30:23 +0300 Subject: [nginx] Proxy: reworked HTTP/0.9 handling. Message-ID: details: http://freenginx.org/hg/nginx/rev/3e14a0ff0d0e branches: changeset: 9399:3e14a0ff0d0e user: Maxim Dounin date: Mon Aug 18 03:18:16 2025 +0300 description: 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. diffstat: src/http/modules/ngx_http_proxy_module.c | 28 ++++++++-------------------- 1 files changed, 8 insertions(+), 20 deletions(-) diffs (40 lines): 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 Mon Aug 18 00:30:23 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:30:23 +0300 Subject: [nginx] Proxy: HTTP/0.9 responses now disabled by default. Message-ID: details: http://freenginx.org/hg/nginx/rev/1d7295b3f072 branches: changeset: 9400:1d7295b3f072 user: Maxim Dounin date: Mon Aug 18 03:18:23 2025 +0300 description: 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. diffstat: src/http/modules/ngx_http_proxy_module.c | 27 +++++++++++++++++++++++---- 1 files changed, 23 insertions(+), 4 deletions(-) diffs (72 lines): 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 Mon Aug 18 00:30:23 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:30:23 +0300 Subject: [nginx] Upstream: simplified ngx_http_upstream_process_header(). Message-ID: details: http://freenginx.org/hg/nginx/rev/ece28b5b9a4b branches: changeset: 9401:ece28b5b9a4b user: Maxim Dounin date: Mon Aug 18 03:18:30 2025 +0300 description: 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. diffstat: src/http/ngx_http_upstream.c | 41 +++++++++++++++++------------------------ 1 files changed, 17 insertions(+), 24 deletions(-) diffs (75 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 @@ -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 Mon Aug 18 00:30:23 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:30:23 +0300 Subject: [nginx] Upstream: simplified u->state->status handling. Message-ID: details: http://freenginx.org/hg/nginx/rev/63b1cef59427 branches: changeset: 9402:63b1cef59427 user: Maxim Dounin date: Mon Aug 18 03:18:37 2025 +0300 description: 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. diffstat: src/http/modules/ngx_http_fastcgi_module.c | 4 ---- src/http/modules/ngx_http_grpc_module.c | 4 ---- src/http/modules/ngx_http_memcached_module.c | 2 -- src/http/modules/ngx_http_proxy_module.c | 8 -------- src/http/modules/ngx_http_scgi_module.c | 8 -------- src/http/modules/ngx_http_uwsgi_module.c | 8 -------- src/http/ngx_http_upstream.c | 1 + 7 files changed, 1 insertions(+), 34 deletions(-) diffs (129 lines): 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 Mon Aug 18 00:30:23 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:30:23 +0300 Subject: [nginx] Upstream: added a function to initialize and clear input... Message-ID: details: http://freenginx.org/hg/nginx/rev/0a1422fdda37 branches: changeset: 9403:0a1422fdda37 user: Maxim Dounin date: Mon Aug 18 03:18:45 2025 +0300 description: 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(). diffstat: src/http/ngx_http_upstream.c | 109 ++++++++++++++++++++++-------------------- 1 files changed, 58 insertions(+), 51 deletions(-) diffs (161 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 @@ -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 Mon Aug 18 00:30:23 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:30:23 +0300 Subject: [nginx] Style. Message-ID: details: http://freenginx.org/hg/nginx/rev/0c49c2d42c8a branches: changeset: 9404:0c49c2d42c8a user: Maxim Dounin date: Mon Aug 18 03:18:50 2025 +0300 description: Style. diffstat: src/http/ngx_http_parse.c | 1 - 1 files changed, 0 insertions(+), 1 deletions(-) diffs (11 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 @@ -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 Mon Aug 18 00:30:23 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:30:23 +0300 Subject: [nginx] Reworked ngx_http_parse_status_line() to avoid data assu... Message-ID: details: http://freenginx.org/hg/nginx/rev/62a1baf5d0b5 branches: changeset: 9405:62a1baf5d0b5 user: Maxim Dounin date: Mon Aug 18 03:18:55 2025 +0300 description: 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. diffstat: src/http/modules/ngx_http_proxy_module.c | 4 --- src/http/modules/ngx_http_scgi_module.c | 15 +----------- src/http/modules/ngx_http_uwsgi_module.c | 15 +----------- src/http/ngx_http.h | 1 - src/http/ngx_http_parse.c | 40 ++++++++++++++++++++++--------- 5 files changed, 30 insertions(+), 45 deletions(-) diffs (184 lines): 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 */ @@ -1797,6 +1815,7 @@ ngx_http_parse_status_line(ngx_http_requ state = sw_almost_done; break; case LF: + status->end = p; goto done; default: return NGX_ERROR; @@ -1810,6 +1829,7 @@ ngx_http_parse_status_line(ngx_http_requ state = sw_almost_done; break; case LF: + status->end = p; goto done; } break; @@ -1835,10 +1855,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 Mon Aug 18 00:30:24 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:30:24 +0300 Subject: [nginx] gRPC: reinitialization of ping and settings limits. Message-ID: details: http://freenginx.org/hg/nginx/rev/47ecba793b8d branches: changeset: 9406:47ecba793b8d user: Maxim Dounin date: Mon Aug 18 03:19:05 2025 +0300 description: 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. diffstat: src/http/modules/ngx_http_grpc_module.c | 2 ++ 1 files changed, 2 insertions(+), 0 deletions(-) diffs (12 lines): 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 Mon Aug 18 00:30:24 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:30:24 +0300 Subject: [nginx] Upstream: unexpected connection upgrades now rejected. Message-ID: details: http://freenginx.org/hg/nginx/rev/f6bcc86eb3bb branches: changeset: 9407:f6bcc86eb3bb user: Maxim Dounin date: Mon Aug 18 03:19:11 2025 +0300 description: 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. diffstat: src/http/modules/ngx_http_fastcgi_module.c | 13 +++++++++++++ src/http/modules/ngx_http_proxy_module.c | 19 ++++++++++++++----- src/http/modules/ngx_http_scgi_module.c | 12 ++++++++++++ src/http/modules/ngx_http_uwsgi_module.c | 12 ++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) diffs (97 lines): 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 Mon Aug 18 00:30:24 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:30:24 +0300 Subject: [nginx] Proxy: connection upgrades now rejected if not configured. Message-ID: details: http://freenginx.org/hg/nginx/rev/ee7201216801 branches: changeset: 9408:ee7201216801 user: Maxim Dounin date: Mon Aug 18 03:19:16 2025 +0300 description: 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). diffstat: src/http/modules/ngx_http_proxy_module.c | 13 ++++++++++++- 1 files changed, 12 insertions(+), 1 deletions(-) diffs (47 lines): 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 Mon Aug 18 00:30:24 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:30:24 +0300 Subject: [nginx] Upstream: unexpected 1xx interim responses now ignored. Message-ID: details: http://freenginx.org/hg/nginx/rev/96ce0adabccb branches: changeset: 9409:96ce0adabccb user: Maxim Dounin date: Mon Aug 18 03:19:28 2025 +0300 description: 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. diffstat: src/http/modules/ngx_http_fastcgi_module.c | 21 ++++++++++-- src/http/modules/ngx_http_grpc_module.c | 47 +++++++++++++++++++++++++---- src/http/modules/ngx_http_proxy_module.c | 23 ++++++++++++-- src/http/modules/ngx_http_scgi_module.c | 22 +++++++++++-- src/http/modules/ngx_http_uwsgi_module.c | 22 +++++++++++-- src/http/ngx_http_upstream.c | 4 +- src/http/ngx_http_upstream.h | 2 + 7 files changed, 115 insertions(+), 26 deletions(-) diffs (265 lines): 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 Mon Aug 18 00:31:03 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:31:03 +0300 Subject: [nginx-tests] Tests: removed bytes sent tests on rejected connec... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/d7a61ea0f425 branches: changeset: 2018:d7a61ea0f425 user: Maxim Dounin date: Mon Aug 18 03:28:29 2025 +0300 description: 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. diffstat: proxy_upgrade.t | 3 +-- ssl_proxy_upgrade.t | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diffs (40 lines): 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 Mon Aug 18 00:31:03 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Mon, 18 Aug 2025 03:31:03 +0300 Subject: [nginx-tests] Tests: upstream status tests. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/b579440daf23 branches: changeset: 2019:b579440daf23 user: Maxim Dounin date: Mon Aug 18 03:28:43 2025 +0300 description: Tests: upstream status tests. diffstat: fastcgi_status.t | 146 +++++++++++++++++++++++++++++++++++++++ grpc_status.t | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ proxy_status.t | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ scgi_status.t | 153 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 699 insertions(+), 0 deletions(-) diffs (719 lines): 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,201 @@ +#!/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 LF /; + +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(13); +$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'); + +# status line without text and trailing space, +# invalid but currently accepted + +like(http_get('/notext'), qr!^HTTP/1.1 200!s, 'status without text'); + +# 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 eq '/notext') { + + print $client + 'HTTP/1.1 200' . LF . + '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 Mon Aug 18 02:46:40 2025 From: mdounin at mdounin.ru (Maxim Dounin) Date: Mon, 18 Aug 2025 05:46:40 +0300 Subject: [PATCH] Added the "proxy_upstream_allow_duplicate_chunked" configuration directive In-Reply-To: <94401755466911@mail.yandex.ru> References: <94401755466911@mail.yandex.ru> Message-ID: Hello! On Mon, Aug 18, 2025 at 12:46:06AM +0300, ??????? ???????? wrote: > # HG changeset patch > # User Gennady Bekasov > # Date 1755456060 -10800 > # Sun Aug 17 21:41:00 2025 +0300 > # Node ID 2a14d182adce88e0ea7bb7ca3bd01e93ee8eee90 > # Parent bdfd605f661eea3d272caf1bd5d85e7c539394ca > Added the "proxy_upstream_allow_duplicate_chunked" configuration > directive. > diff -r bdfd605f661e -r 2a14d182adce > src/http/modules/ngx_http_proxy_module.c > --- a/src/http/modules/ngx_http_proxy_module.c Wed Jul 23 21:53:19 > 2025 +0300 > +++ b/src/http/modules/ngx_http_proxy_module.c Sun Aug 17 21:41:00 > 2025 +0300 > @@ -367,6 +367,13 @@ > offsetof(ngx_http_proxy_loc_conf_t, > upstream.ignore_client_abort), > NULL }, > > + { ngx_string("proxy_upstream_allow_duplicate_chunked"), > + > 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, > upstream.upstream_allow_duplicate_chunked), > + NULL }, > + I don't think that "proxy_upstream_allow_duplicate_chunked" is a good name for the directive, just "proxy_allow_duplicate_chunked" would be enough. > { ngx_string("proxy_bind"), > NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_T > AKE12, > ngx_http_upstream_bind_set_slot, > @@ -3364,6 +3371,7 @@ > conf->upstream.request_buffering = NGX_CONF_UNSET; > conf->upstream.ignore_client_abort = NGX_CONF_UNSET; > conf->upstream.force_ranges = NGX_CONF_UNSET; > + conf->upstream.upstream_allow_duplicate_chunked = NGX_CONF_UNSET; > > conf->upstream.local = NGX_CONF_UNSET_PTR; > conf->upstream.socket_keepalive = NGX_CONF_UNSET; > @@ -3494,6 +3502,9 @@ > ngx_conf_merge_value(conf->upstream.force_ranges, > prev->upstream.force_ranges, 0); > > + > ngx_conf_merge_value(conf->upstream.upstream_allow_duplicate_chunked, > + > prev->upstream.upstream_allow_duplicate_chunked, 0); > + > ngx_conf_merge_ptr_value(conf->upstream.local, > prev->upstream.local, NULL); > > diff -r bdfd605f661e -r 2a14d182adce src/http/ngx_http_upstream.c > --- a/src/http/ngx_http_upstream.c Wed Jul 23 21:53:19 2025 +0300 > +++ b/src/http/ngx_http_upstream.c Sun Aug 17 21:41:00 2025 +0300 > @@ -5300,7 +5300,11 @@ > &h->key, &h->value, > &u->headers_in.transfer_encoding->key, > &u->headers_in.transfer_encoding->value); > - return NGX_HTTP_UPSTREAM_INVALID_HEADER; > + if (!u->conf->upstream_allow_duplicate_chunked) { > + return NGX_HTTP_UPSTREAM_INVALID_HEADER; > + } > + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, > + "insecure data exchange due to pass duplicate > \"Transfer-Encoding\" header as enabled by > upstream_allow_duplicate_chunked parameter"); > } > > if (u->headers_in.content_length) { That's certainly too chatty: there is a message at the "error" level, and adding another one at "alert", which is expected to be used for serious issues, such as unexpected system or library errors, or internal bugs, is certainly not the way to go. At most there should be just one "warn" here with "proxy_allow_duplicate_chunked on" in the configuration. But given that it is "off" by default, and needs to be explicitly enabled, it might be good enough to not log it at all. > diff -r bdfd605f661e -r 2a14d182adce src/http/ngx_http_upstream.h > --- a/src/http/ngx_http_upstream.h Wed Jul 23 21:53:19 2025 +0300 > +++ b/src/http/ngx_http_upstream.h Sun Aug 17 21:41:00 2025 +0300 > @@ -177,6 +177,7 @@ > ngx_flag_t request_buffering; > ngx_flag_t pass_request_headers; > ngx_flag_t pass_request_body; > + ngx_flag_t upstream_allow_duplicate_chunked; > > ngx_flag_t ignore_client_abort; > ngx_flag_t intercept_errors; I would rather put it somewhere after the next block of flags, which looks more related. Patch with the above comments incorporated (and more): # HG changeset patch # User Maxim Dounin # Date 1755481476 -10800 # Mon Aug 18 04:44:36 2025 +0300 # Node ID 0983c4766d832640556a27df8855cd707dd80e7b # Parent 96ce0adabccbb0cad7a0150598b933ddd6026070 Proxy: added the "proxy_allow_duplicate_chunked" directive. This directive allows to accept duplicate "Transfer-Encoding: chunked" header lines. These are invalid, and rejected since 8033:2bf7792c262e (1.23.0), yet it turns out there are quite a few homegrown proxies, notably Java-based ones, which emit such duplicate headers. The "proxy_allow_duplicate_chunked" directive makes it possible to enable compatibility with such proxies by ignoring duplicate "Transfer-Encoding: chunked" headers instead of rejecting them. Prodded by Gennady Bekasov, https://github.com/freenginx/nginx/issues/11 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 @@ -2948,10 +2948,10 @@ ngx_http_fastcgi_create_loc_conf(ngx_con conf->upstream.intercept_errors = NGX_CONF_UNSET; - /* "fastcgi_cyclic_temp_file" is disabled */ + /* the hardcoded values */ conf->upstream.cyclic_temp_file = 0; - conf->upstream.change_buffering = 1; + conf->upstream.duplicate_chunked = 0; conf->catch_stderr = NGX_CONF_UNSET_PTR; 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 @@ -4431,6 +4431,7 @@ ngx_http_grpc_create_loc_conf(ngx_conf_t conf->upstream.pass_request_headers = 1; conf->upstream.pass_request_body = 1; conf->upstream.force_ranges = 0; + conf->upstream.duplicate_chunked = 0; conf->upstream.pass_trailers = 1; conf->upstream.preserve_output = 1; 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 @@ -627,6 +627,7 @@ ngx_http_memcached_create_loc_conf(ngx_c conf->upstream.pass_request_headers = 0; conf->upstream.pass_request_body = 0; conf->upstream.force_ranges = 1; + conf->upstream.duplicate_chunked = 0; conf->index = NGX_CONF_UNSET; conf->gzip_flag = NGX_CONF_UNSET_UINT; 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 @@ -695,6 +695,13 @@ static ngx_command_t ngx_http_proxy_com offsetof(ngx_http_proxy_loc_conf_t, http09), NULL }, + { ngx_string("proxy_allow_duplicate_chunked"), + 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, upstream.duplicate_chunked), + NULL }, + #if (NGX_HTTP_SSL) { ngx_string("proxy_ssl_session_reuse"), @@ -3392,6 +3399,7 @@ ngx_http_proxy_create_loc_conf(ngx_conf_ conf->upstream.request_buffering = NGX_CONF_UNSET; conf->upstream.ignore_client_abort = NGX_CONF_UNSET; conf->upstream.force_ranges = NGX_CONF_UNSET; + conf->upstream.duplicate_chunked = NGX_CONF_UNSET; conf->upstream.local = NGX_CONF_UNSET_PTR; conf->upstream.socket_keepalive = NGX_CONF_UNSET; @@ -3444,9 +3452,8 @@ ngx_http_proxy_create_loc_conf(ngx_conf_ conf->ssl_conf_commands = NGX_CONF_UNSET_PTR; #endif - /* "proxy_cyclic_temp_file" is disabled */ + /* the hardcoded values */ conf->upstream.cyclic_temp_file = 0; - conf->upstream.change_buffering = 1; conf->headers_source = NGX_CONF_UNSET_PTR; @@ -3523,6 +3530,9 @@ ngx_http_proxy_merge_loc_conf(ngx_conf_t ngx_conf_merge_value(conf->upstream.force_ranges, prev->upstream.force_ranges, 0); + ngx_conf_merge_value(conf->upstream.duplicate_chunked, + prev->upstream.duplicate_chunked, 0); + ngx_conf_merge_ptr_value(conf->upstream.local, prev->upstream.local, NULL); 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 @@ -1334,10 +1334,10 @@ ngx_http_scgi_create_loc_conf(ngx_conf_t conf->upstream.intercept_errors = NGX_CONF_UNSET; - /* "scgi_cyclic_temp_file" is disabled */ + /* the hardcoded values */ conf->upstream.cyclic_temp_file = 0; - conf->upstream.change_buffering = 1; + conf->upstream.duplicate_chunked = 0; ngx_str_set(&conf->upstream.module, "scgi"); 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 @@ -1578,10 +1578,10 @@ ngx_http_uwsgi_create_loc_conf(ngx_conf_ conf->ssl_conf_commands = NGX_CONF_UNSET_PTR; #endif - /* "uwsgi_cyclic_temp_file" is disabled */ + /* the hardcoded values */ conf->upstream.cyclic_temp_file = 0; - conf->upstream.change_buffering = 1; + conf->upstream.duplicate_chunked = 0; ngx_str_set(&conf->upstream.module, "uwsgi"); 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 @@ -5292,7 +5292,7 @@ ngx_http_upstream_process_transfer_encod u = r->upstream; - if (u->headers_in.transfer_encoding) { + if (u->headers_in.transfer_encoding && !u->conf->duplicate_chunked) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "upstream sent duplicate header line: \"%V: %V\", " "previous value: \"%V: %V\"", 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 @@ -182,6 +182,7 @@ typedef struct { ngx_flag_t intercept_errors; ngx_flag_t cyclic_temp_file; ngx_flag_t force_ranges; + ngx_flag_t duplicate_chunked; ngx_path_t *temp_path; And tests: # HG changeset patch # User Maxim Dounin # Date 1755485120 -10800 # Mon Aug 18 05:45:20 2025 +0300 # Node ID 29e233ff3ad706c93210884b99cbc80840537bbc # Parent b579440daf23d27ff4a72d33e01370d0eac101cf Tests: tests for the "proxy_allow_duplicate_chunked" directive. diff --git a/proxy_duplicate_chunked.t b/proxy_duplicate_chunked.t new file mode 100644 --- /dev/null +++ b/proxy_duplicate_chunked.t @@ -0,0 +1,121 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Test for http backend returning response with duplicate "Transfer-Encoding: +# chunked" headers and the "proxy_allow_duplicate_chunked" 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 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; + + location / { + proxy_pass http://127.0.0.1:8081; + proxy_read_timeout 1s; + } + + location /allow/ { + proxy_pass http://127.0.0.1:8081; + proxy_read_timeout 1s; + proxy_allow_duplicate_chunked on; + } + } +} + +EOF + +$t->run_daemon(\&http_daemon); +$t->try_run('no proxy_allow_duplicate_chunked')->plan(3); +$t->waitforsocket('127.0.0.1:' . port(8081)); + +############################################################################### + +like(http_get('/'), qr/200 OK/, 'normal'); + +like(http_get('/duplicate-chunked'), qr/502 Bad/, + 'duplicate transfer encoding'); + +like(http_get('/allow/duplicate-chunked'), qr/200 OK/, + 'duplicate transfer encoding allowed'); + +############################################################################### + +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 . + 'Content-Length: 0' . CRLF . CRLF; + + } elsif ($uri =~ m/duplicate-chunked/) { + + print $client + 'HTTP/1.1 200 OK' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + '0' . CRLF . CRLF; + + } + + close $client; + } +} + +############################################################################### -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Mon Aug 18 16:39:26 2025 From: mdounin at mdounin.ru (Maxim Dounin) Date: Mon, 18 Aug 2025 19:39:26 +0300 Subject: [PATCH 2 of 2] SSL: support for iPAddress subjectAltName in certificates In-Reply-To: <870dfc16d381d90644b0.1755186722@vm-bsd.mdounin.ru> References: <51d23ff6f109765f4c38.1755186721@vm-bsd.mdounin.ru> <870dfc16d381d90644b0.1755186722@vm-bsd.mdounin.ru> Message-ID: Hello! On Thu, Aug 14, 2025 at 06:52:02PM +0300, Maxim Dounin wrote: > # 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; > + } > } > } > > Amended with the following patch, to provide more natural order of altname->type handling: 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 @@ -4994,7 +4994,23 @@ 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_IPADD) { + 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; + } + + } else if (altname->type == GEN_IPADD) { str = altname->d.iPAddress; data = ASN1_STRING_data(str); @@ -5035,22 +5051,6 @@ ngx_ssl_check_host(ngx_connection_t *c, 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; - } } } -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Mon Aug 18 23:51:32 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 19 Aug 2025 02:51:32 +0300 Subject: [PATCH 0 of 4] host validation cleanup Message-ID: Hello! The following patch series improves ngx_http_validate_host(). It now requires the Host header to be valid according to RFC 3986, instead of accepting more or less anything except ".." and "/". In particular, non-ASCII characters and invalid port numbers are no longer allowed in the Host header. This also fixes "." not being stripped if port contained ".", which can be seen as an issue in some configurations, see https://github.com/freenginx/nginx/issues/11. At the same time, the patch series relaxes checks in request line parsing to actually accept hosts which are valid per RFC 3986 (and RFC 6874, IPv6 addresses with zone identifiers). Previously, much stricter rules were used, and some valid hosts were rejected in the request line - notably, hosts with "_", which are used in practice. This wasn't really noticed though, since the absolute form of the request URI isn't used much. Review and testing appreciated. -- Maxim Dounin From mdounin at mdounin.ru Mon Aug 18 23:51:33 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 19 Aug 2025 02:51:33 +0300 Subject: [PATCH 1 of 4] Updated request line parsing to allow IPv6 zone identifiers In-Reply-To: References: Message-ID: <5cc30c1b91dba2a4358a.1755561093@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1755535190 -10800 # Mon Aug 18 19:39:50 2025 +0300 # Node ID 5cc30c1b91dba2a4358a74093300697ecda9ec39 # Parent 96ce0adabccbb0cad7a0150598b933ddd6026070 Updated request line parsing to allow IPv6 zone identifiers. RFC 6874 defines syntax to allow IPv6 zone identifiers in URIs, by providing an additional option in IP-literal: IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture ) "]" ZoneID = 1*( unreserved / pct-encoded ) IPv6addrz = IPv6address "%25" ZoneID The "%" character wasn't previously allowed in IP literals, and therefore attempts to use such addresses in request line resulted in 400 (Bad Request) (but was accepted in the Host header field, which uses more relaxed parsing). With this change, "%" is now allowed in IP literals. 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 @@ -466,6 +466,9 @@ ngx_http_parse_request_line(ngx_http_req case '=': /* sub-delims */ break; + case '%': + /* pct-encoded */ + break; default: return NGX_HTTP_PARSE_INVALID_REQUEST; } From mdounin at mdounin.ru Mon Aug 18 23:51:34 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 19 Aug 2025 02:51:34 +0300 Subject: [PATCH 2 of 4] Updated request line parsing to allow uncommon chars in host In-Reply-To: References: Message-ID: <097c394fa0fd14169bbe.1755561094@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1755535197 -10800 # Mon Aug 18 19:39:57 2025 +0300 # Node ID 097c394fa0fd14169bbe054c469886213cbaabad # Parent 5cc30c1b91dba2a4358a74093300697ecda9ec39 Updated request line parsing to allow uncommon chars in host. Previously, only ALPHA, DIGIT, ".", and "-" were allowed in the host component of the request line (if it's not an IP literal). On the other hand, RFC 3986 allows the following: reg-name = *( unreserved / pct-encoded / sub-delims ) unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" pct-encoded = "%" HEXDIG HEXDIG Notably, the "_" character is used in practice but was not allowed in the request line. At the same time, this and other characters do actually work in practice, as they are accepted in the Host header field, which uses more relaxed parsing. With this change, all characters which are valid in the host name per RFC 3986 are also allowed in the request line. 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 @@ -392,7 +392,16 @@ ngx_http_parse_request_line(ngx_http_req break; } - if ((ch >= '0' && ch <= '9') || ch == '.' || ch == '-') { + if (ch >= '0' && ch <= '9') { + break; + } + + if (ch == '.' || ch == '-' || ch == '_' || ch == '~' + || ch == '!' || ch == '$' || ch == '&' || ch == '\'' + || ch == '(' || ch == ')' || ch == '*' || ch == '+' + || ch == ',' || ch == ';' || ch == '=' || ch == '%') + { + /* unreserved, sub-delims, pct-encoded */ break; } From mdounin at mdounin.ru Mon Aug 18 23:51:35 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 19 Aug 2025 02:51:35 +0300 Subject: [PATCH 3 of 4] Updated request line parsing to use if() in IP literals In-Reply-To: References: Message-ID: # HG changeset patch # User Maxim Dounin # Date 1755535220 -10800 # Mon Aug 18 19:40:20 2025 +0300 # Node ID fe761d14f0f28e37f1bb81c7cfb3464df68d2f84 # Parent 097c394fa0fd14169bbe054c469886213cbaabad Updated request line parsing to use if() in IP literals. Using if() results in more readable code and matches the code used in host parsing, where it is not convenient to use switch(). Besides, it is slightly faster than switch() on typical inputs. 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 @@ -450,34 +450,23 @@ ngx_http_parse_request_line(ngx_http_req break; } + if (ch == ':') { + break; + } + + if (ch == '.' || ch == '-' || ch == '_' || ch == '~' + || ch == '!' || ch == '$' || ch == '&' || ch == '\'' + || ch == '(' || ch == ')' || ch == '*' || ch == '+' + || ch == ',' || ch == ';' || ch == '=' || ch == '%') + { + /* unreserved, sub-delims, pct-encoded */ + break; + } + switch (ch) { - case ':': - break; case ']': state = sw_host_end; break; - case '-': - case '.': - case '_': - case '~': - /* unreserved */ - break; - case '!': - case '$': - case '&': - case '\'': - case '(': - case ')': - case '*': - case '+': - case ',': - case ';': - case '=': - /* sub-delims */ - break; - case '%': - /* pct-encoded */ - break; default: return NGX_HTTP_PARSE_INVALID_REQUEST; } From mdounin at mdounin.ru Mon Aug 18 23:51:36 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 19 Aug 2025 02:51:36 +0300 Subject: [PATCH 4 of 4] Improved ngx_http_validate_host() to do better syntax validation In-Reply-To: References: Message-ID: <7c31ad5dcb415c67a64c.1755561096@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1755535263 -10800 # Mon Aug 18 19:41:03 2025 +0300 # Node ID 7c31ad5dcb415c67a64cffedf1281902863745fd # Parent fe761d14f0f28e37f1bb81c7cfb3464df68d2f84 Improved ngx_http_validate_host() to do better syntax validation. With this change, syntax validation in ngx_http_validate_host() is mostly identical to the one used for the request line parsing, which now follows RFC 3986. In particular, as a result of this change non-ASCII characters and invalid port numbers are no longer allowed in the Host header. This also fixes "." not being stripped if port contained ".", as such requests are now rejected by syntax checks. Prodded by Anton Mironenko, https://github.com/freenginx/nginx/issues/11 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 @@ -2155,9 +2155,11 @@ ngx_http_validate_host(ngx_str_t *host, size_t i, dot_pos, host_len; enum { - sw_usual = 0, + sw_start = 0, + sw_host, + sw_host_end, sw_literal, - sw_rest + sw_port } state; dot_pos = host->len; @@ -2165,55 +2167,126 @@ ngx_http_validate_host(ngx_str_t *host, h = host->data; - state = sw_usual; + state = sw_start; for (i = 0; i < host->len; i++) { ch = h[i]; - switch (ch) { - - case '.': - if (dot_pos == i - 1) { - return NGX_DECLINED; - } - dot_pos = i; - break; - - case ':': - if (state == sw_usual) { - host_len = i; - state = sw_rest; - } - break; - - case '[': - if (i == 0) { + switch (state) { + + case sw_start: + + if (ch == '[') { + host_len = 0; state = sw_literal; - } - break; - - case ']': - if (state == sw_literal) { - host_len = i + 1; - state = sw_rest; + break; } - break; - - default: - - if (ngx_path_separator(ch)) { - return NGX_DECLINED; - } - - if (ch <= 0x20 || ch == 0x7f) { - return NGX_DECLINED; + + state = sw_host; + + /* fall through */ + + case sw_host: + + if (ch >= 'a' && ch <= 'z') { + break; } if (ch >= 'A' && ch <= 'Z') { alloc = 1; + break; } - break; + if (ch >= '0' && ch <= '9') { + break; + } + + if (ch == '.') { + if (dot_pos == i - 1) { + return NGX_DECLINED; + } + dot_pos = i; + break; + } + + if (ch == '-' || ch == '_' || ch == '~' + || ch == '!' || ch == '$' || ch == '&' || ch == '\'' + || ch == '(' || ch == ')' || ch == '*' || ch == '+' + || ch == ',' || ch == ';' || ch == '=' || ch == '%') + { + /* unreserved, sub-delims, pct-encoded */ + break; + } + + /* fall through */ + + case sw_host_end: + + host_len = i; + + if (ch == ':') { + state = sw_port; + break; + } + + /* notably, "/" and "\" are rejected */ + + return NGX_DECLINED; + + case sw_literal: + + if (ch >= '0' && ch <= '9') { + break; + } + + if (ch >= 'a' && ch <= 'z') { + break; + } + + if (ch >= 'A' && ch <= 'Z') { + alloc = 1; + break; + } + + if (ch == ':') { + break; + } + + if (ch == '.') { + if (dot_pos == i - 1) { + return NGX_DECLINED; + } + dot_pos = i; + break; + } + + if (ch == '-' || ch == '_' || ch == '~' + || ch == '!' || ch == '$' || ch == '&' || ch == '\'' + || ch == '(' || ch == ')' || ch == '*' || ch == '+' + || ch == ',' || ch == ';' || ch == '=' || ch == '%') + { + /* unreserved, sub-delims, pct-encoded */ + break; + } + + if (ch == ']') { + host_len = i + 1; + state = sw_host_end; + break; + } + + /* notably, "/" and "\" are rejected */ + + return NGX_DECLINED; + + case sw_port: + + if (ch >= '0' && ch <= '9') { + break; + } + + return NGX_DECLINED; + } } From mdounin at mdounin.ru Tue Aug 19 00:24:00 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Tue, 19 Aug 2025 03:24:00 +0300 Subject: [PATCH] Tests: rewritten http_host.t In-Reply-To: References: Message-ID: <5a89006b7b8bdbe94d9a.1755563040@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1755535605 -10800 # Mon Aug 18 19:46:45 2025 +0300 # Node ID 5a89006b7b8bdbe94d9af5f2724bf4542888bcf0 # Parent b579440daf23d27ff4a72d33e01370d0eac101cf Tests: rewritten http_host.t. Resulting checks are expected to be more consistent, and test for all possible cases via the Host header and via the request line (as long as it is possible). Tests now also cover recent changes to ngx_http_parse_request_line() (more relaxed parsing, matching RFC 3986) and in the Host header (stricter parsing, matching RFC 3986). diff --git a/http_host.t b/http_host.t --- a/http_host.t +++ b/http_host.t @@ -22,7 +22,7 @@ use Test::Nginx qw/ :DEFAULT http_conten select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http rewrite/)->plan(37); +my $t = Test::Nginx->new()->has(qw/http rewrite/)->plan(74); $t->write_file_expand('nginx.conf', <<'EOF'); @@ -52,146 +52,272 @@ EOF ############################################################################### -is(http_host_header('www.abcd-ef.g02.xyz'), 'www.abcd-ef.g02.xyz', - 'domain w/o port (host header)'); -is(http_host_header('abcd-ef.g02.xyz:' . port(8080)), 'abcd-ef.g02.xyz', - 'domain w/port (host header)'); +# host +# host:port +# host. +# host.:port +# HOST +# HOST:port +# host09 +# ho-st +# _host + +is(hh('example.com'), 'example.com', 'host'); +is(rl('example.com'), 'example.com', 'host in request line'); -is(http_absolute_path('abcd-ef.g02.xyz'), 'abcd-ef.g02.xyz', - 'domain w/o port (absolute request)'); -is(http_absolute_path('www.abcd-ef.g02.xyz:10'), 'www.abcd-ef.g02.xyz', - 'domain w/port (absolute request)'); +is(hh('example.com:80'), 'example.com', 'host:port'); +is(rl('example.com:80'), 'example.com', 'host:port in request line'); + +is(hh('example.com.'), 'example.com', 'host with dot'); +is(rl('example.com.'), 'example.com', 'host with dot in request line'); +is(hh('example.com.:80'), 'example.com', 'host:port with dot'); +is(rl('example.com.:80'), 'example.com', 'host:port with dot in request line'); -is(http_host_header('www.abcd-ef.g02.xyz.'), 'www.abcd-ef.g02.xyz', - 'domain w/ ending dot w/o port (host header)'); +is(hh('EXAMPLE.com'), 'example.com', 'host with uppercase'); +is(rl('EXAMPLE.com'), 'example.com', 'host with uppercase in request line'); + +is(hh('EXAMPLE.com:80'), 'example.com', 'host:port with uppercase'); +is(rl('EXAMPLE.com:80'), 'example.com', + 'host:port with uppercase in request line'); -is(http_host_header('abcd-ef.g02.xyz.:88'), 'abcd-ef.g02.xyz', - 'domain w/ ending dot w/port (host header)'); +is(hh('foo09.example.com'), 'foo09.example.com', 'host with digits'); +is(rl('foo09.example.com'), 'foo09.example.com', + 'host with digits in request line'); + +is(hh('foo-bar.example.com'), 'foo-bar.example.com', 'host with dash'); +is(rl('foo-bar.example.com'), 'foo-bar.example.com', + 'host with dash in request line'); + +is(hh('_foo.example.com'), '_foo.example.com', 'host with underscore'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +is(rl('_foo.example.com'), '_foo.example.com', + 'host with underscore in request line'); -is(http_absolute_path('www.abcd-ef.g02.xyz.'), 'www.abcd-ef.g02.xyz', - 'domain w/ ending dot w/o port (absolute request)'); -is(http_absolute_path('abcd-ef.g02.xyz.:2'), 'abcd-ef.g02.xyz', - 'domain w/ ending dot w/port (absolute request)'); +} +# all characters permitted by RFC 3986 +# (unreserved, pct-encoded, sub-delims) + +is(hh(q{-._~!$&'()*+,;=%25.example.com}), q{-._~!$&'()*+,;=%25.example.com}, + 'host with sub-delims'); -is(http_absolute_path('AbC-d93.0.34ZhGt-s.nk.Ru'), 'abc-d93.0.34zhgt-s.nk.ru', - 'mixed case domain w/o port (absolute request)'); -is(http_host_header('AbC-d93.0.34ZhGt-s.nk.Ru:88'), 'abc-d93.0.34zhgt-s.nk.ru', - 'mixed case domain w/port (host header)'); +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); +is(rl(q{-._~!$&'()*+,;=%25.example.com}), q{-._~!$&'()*+,;=%25.example.com}, + 'host with sub-delims in request line'); + +} -is(http_host_header('123.40.56.78'), '123.40.56.78', - 'ipv4 w/o port (host header)'); -is(http_host_header('123.49.0.78:987'), '123.49.0.78', - 'ipv4 w/port (host header)'); +# ip +# ip:port +# ipv6 +# ipv6:port +# ipv6-v4mapped +# ipv6-v4mapped:port + +is(hh('192.0.2.1'), '192.0.2.1', 'ip'); +is(rl('192.0.2.1'), '192.0.2.1', 'ip in request line'); + +is(hh('192.0.2.1:80'), '192.0.2.1', 'ip'); +is(rl('192.0.2.1:80'), '192.0.2.1', 'ip:port in request line'); -is(http_absolute_path('123.49.0.78'), '123.49.0.78', - 'ipv4 w/o port (absolute request)'); -is(http_absolute_path('123.40.56.78:123'), '123.40.56.78', - 'ipv4 w/port (absolute request)'); +is(hh('[2001:db8::1]'), '[2001:db8::1]', 'ipv6'); +is(rl('[2001:db8::1]'), '[2001:db8::1]', 'ipv6 in request line'); + +is(hh('[2001:db8::1]:80'), '[2001:db8::1]', 'ipv6:port'); +is(rl('[2001:db8::1]:80'), '[2001:db8::1]', 'ipv6:port in request line'); -is(http_host_header('[abcd::ef98:0:7654:321]'), '[abcd::ef98:0:7654:321]', - 'ipv6 literal w/o port (host header)'); -is(http_host_header('[abcd::ef98:0:7654:321]:80'), '[abcd::ef98:0:7654:321]', - 'ipv6 literal w/port (host header)'); +is(hh('[2001:DB8::1]'), '[2001:db8::1]', 'ipv6 with uppercase'); +is(rl('[2001:DB8::1]'), '[2001:db8::1]', + 'ipv6 with uppercase in request line'); + +is(hh('[2001:DB8::1]:80'), '[2001:db8::1]', 'ipv6:port with uppercase'); +is(rl('[2001:DB8::1]:80'), '[2001:db8::1]', + 'ipv6:port with uppercase in request line'); -is(http_absolute_path('[abcd::ef98:0:7654:321]'), '[abcd::ef98:0:7654:321]', - 'ipv6 literal w/o port (absolute request)'); -is(http_absolute_path('[abcd::ef98:0:7654:321]:5'), '[abcd::ef98:0:7654:321]', - 'ipv6 literal w/port (absolute request)'); +is(hh('[::ffff:192.0.2.1]'), '[::ffff:192.0.2.1]', 'ipv6 v4mapped'); +is(rl('[::ffff:192.0.2.1]'), '[::ffff:192.0.2.1]', + 'ipv6 v4mapped in request line'); + +is(hh('[::ffff:192.0.2.1]:80'), '[::ffff:192.0.2.1]', 'ipv6:port v4mapped '); +is(rl('[::ffff:192.0.2.1]:80'), '[::ffff:192.0.2.1]', + 'ipv6:port v4mapped in request line'); + +# ipv6 with zoneid, RFC 6874 + +is(hh('[2001:db8::1%25en1]'), '[2001:db8::1%25en1]', 'ipv6 zoneid'); +is(hh('[2001:db8::1%25en1]:80'), '[2001:db8::1%25en1]', 'ipv6:port zoneid'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); -is(http_host_header('[::ffff:12.30.67.89]'), '[::ffff:12.30.67.89]', - 'ipv4-mapped ipv6 w/o port (host header)'); -is(http_host_header('[::123.45.67.89]:4321'), '[::123.45.67.89]', - 'ipv4-mapped ipv6 w/port (host header)'); +is(rl('[2001:db8::1%25en1]'), '[2001:db8::1%25en1]', + 'ipv6 zoneid in request line'); +is(rl('[2001:db8::1%25en1]:80'), '[2001:db8::1%25en1]', + 'ipv6:port zoneid in request line'); +} + +# ipvfuture -is(http_absolute_path('[::123.45.67.89]'), '[::123.45.67.89]', - 'ipv4-mapped ipv6 w/o port (absolute request)'); -is(http_absolute_path('[::ffff:12.30.67.89]:4321'), '[::ffff:12.30.67.89]', - 'ipv4-mapped ipv6 w/port (absolute request)'); +is(hh('[v0.1azAZ.!$&\'()*+,;=-._~:]'), '[v0.1azaz.!$&\'()*+,;=-._~:]', + 'ipvfuture'); +is(rl('[v0.1azAZ.!$&\'()*+,;=-._~:]'), '[v0.1azaz.!$&\'()*+,;=-._~:]', + 'ipvfuture in request line'); + +is(hh('[v0.1azAZ.!$&\'()*+,;=-._~:]:80'), '[v0.1azaz.!$&\'()*+,;=-._~:]', + 'ipvfuture:port'); +is(rl('[v0.1azAZ.!$&\'()*+,;=-._~:]:80'), '[v0.1azaz.!$&\'()*+,;=-._~:]', + 'ipvfuture:port in request line'); -like(http_host_header('example.com/\:552', 1), qr/ 400 /, - 'domain w/ path separators (host header)'); -like(http_absolute_path('\e/xample.com', 1), qr/ 400 /, - 'domain w/ path separators (absolute request)'); +# various invalid cases: +# +# example/com (only make sense in host header) +# example\com +# example..com +# example.com:port:port +# example.com:invalid_port +# [ipv6/foo] +# [ipvfuture/foo] +# [ipv6..foo] +# [ipvfuture..foo] +# [ipv6 (no closing "]") +# [ipvfuture (no closing "]") -like(http_host_header('..examp-LE.com', 1), qr/ 400 /, - 'domain w/ double dot (host header)'); -like(http_absolute_path('com.exa-m.45..:', 1), qr/ 400 /, - 'domain w/ double dot (absolute request)'); +like(hh('example/com'), qr/ 400 /, 'host with slash'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1') + or $^O eq 'MSWin32'; +like(hh('example\com'), qr/ 400 /, 'host with backslash'); + +} + +like(rl('example\com'), qr/ 400 /, 'host with backslash in request line'); + +like(hh('example..com'), qr/ 400 /, 'host with double dots'); +like(rl('example..com'), qr/ 400 /, 'host with double dots in request line'); -like(http_host_header('[abcd::e\f98:0/:7654:321]', 1), qr/ 400 /, - 'ipv6 literal w/ path separators (host header)'); -like(http_absolute_path('[abcd\::ef98:0:7654:321/]:12', 1), qr/ 400 /, - 'ipv6 literal w/ path separators (absolute request)'); +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +like(hh('example.com:80:80'), qr/ 400 /, 'host with two ports'); + +} + +like(rl('example.com:80:80'), qr/ 400 /, + 'host with two ports in request line'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +like(hh('example.com:foo'), qr/ 400 /, 'host with invalid port'); -like(http_host_header('[abcd::ef98:0:7654:321]..:98', 1), qr/ 400 /, - 'ipv6 literal w/ double dot (host header)'); -like(http_absolute_path('[ab..cd::ef98:0:7654:321]', 1), qr/ 400 /, - 'ipv6 literal w/ double dot (absolute request)'); +} +like(rl('example.com:foo'), qr/ 400 /, + 'host with invalid port in request line'); -like(http_host_header('[abcd::ef98:0:7654:321]..:98', 1), qr/ 400 /, - 'ipv6 literal w/ double dot (host header)'); -like(http_absolute_path('[ab..cd::ef98:0:7654:321]', 1), qr/ 400 /, - 'ipv6 literal w/ double dot (absolute request)'); +like(hh('[2001:db8::1/2]'), qr/ 400 /, 'ipv6 with slash'); +like(rl('[2001:db8::1/2]'), qr/ 400 /, + 'ipv6 with slash in request line'); +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1') + or $^O eq 'MSWin32'; + +like(hh('[2001:db8::1\2]'), qr/ 400 /, 'ipv6 with backslash'); + +} -# As per RFC 3986, -# http://tools.ietf.org/html/rfc3986#section-3.2.2 -# -# IP-literal = "[" ( IPv6address / IPvFuture ) "]" -# -# IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) -# -# sub-delims = "!" / "$" / "&" / "'" / "(" / ")" -# / "*" / "+" / "," / ";" / "=" -# -# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -# +like(rl('[2001:db8::1\2]'), qr/ 400 /, 'ipv6 with backslash in request line'); + +like(hh('[2001:db8::1..2]'), qr/ 400 /, 'ipv6 with double dots'); +like(rl('[2001:db8::1..2]'), qr/ 400 /, + 'ipv6 with double dots in request line'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +like(hh('[2001:db8::1'), qr/ 400 /, 'ipv6 without closing bracket'); + +} + +like(rl('[2001:db8::1'), qr/ 400 /, + 'ipv6 without closing bracket in request line'); + +like(hh('[v0.1/2]'), qr/ 400 /, 'ipvfuture with slash'); +like(rl('[v0.1/2]'), qr/ 400 /, 'ipvfuture with slash in request line'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1') + or $^O eq 'MSWin32'; + +like(hh('[v0.1\2]'), qr/ 400 /, 'ipvfuture with backslash'); + +} + +like(rl('[v0.1\2]'), qr/ 400 /, 'ipvfuture with backslash in request line'); + +like(hh('[v0.1..2]'), qr/ 400 /, 'ipvfuture with double dots'); +like(rl('[v0.1..2]'), qr/ 400 /, 'ipvfuture with double dots in request line'); -is(http_host_header( - '[v0123456789aBcDeF.!$&\'()*+,;=-._~AbCdEfGhIjKlMnOpQrStUvWxYz' - . '0123456789:]'), - '[v0123456789abcdef.!$&\'()*+,;=-._~abcdefghijklmnopqrstuvwxyz' - . '0123456789:]', - 'IPvFuture all symbols (host header)'); +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +like(hh('[v0.1'), qr/ 400 /, 'ipvfuture without closing bracket'); + +} + +like(rl('[v0.1'), qr/ 400 /, + 'ipvfuture without closing bracket in request line'); + +# control characters + +like(hh("example.com\x02"), qr/ 400 /, 'host with control chars'); +like(rl("example.com\x02"), qr/ 400 /, + 'host with control chars in request line'); -is(http_absolute_path( - '[v0123456789aBcDeF.!$&\'()*+,;=-._~AbCdEfGhIjKlMnOpQrStUvWxYz' - . '0123456789:]'), - '[v0123456789abcdef.!$&\'()*+,;=-._~abcdefghijklmnopqrstuvwxyz' - . '0123456789:]', - 'IPvFuture all symbols (absolute request)'); +# non-ascii characters + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +like(hh("example.com\xff"), qr/ 400 /, 'host with non-ascii chars'); -is(http_host_header('123.40.56.78:9000:80'), '123.40.56.78', - 'double port hack'); +} -like(http_host_header("localhost\nHost: again", 1), qr/ 400 /, 'host repeat'); -like(http_host_header("localhost\x02", 1), qr/ 400 /, 'control'); +like(rl("example.com\xff"), qr/ 400 /, + 'host with non-ascii chars in request line'); + +# multiple host headers + +like(hh("localhost\nHost: again"), qr/ 400 /, 'duplicate host'); ############################################################################### -sub http_host_header { - my ($host, $all) = @_; - my ($r) = http(< # HG changeset patch # User Maxim Dounin # Date 1755563152 -10800 # Tue Aug 19 03:25:52 2025 +0300 # Node ID 250fb78dd27079ed3cb3fd5d3b7a0132ad2ce89f # Parent b579440daf23d27ff4a72d33e01370d0eac101cf Tests: adjusted delays in body discard tests. At least h3_request_body_discard.t occasionally fails on slow hosts due to too short delays, and using longer delays fixes this. diff --git a/body_discard.t b/body_discard.t --- a/body_discard.t +++ b/body_discard.t @@ -350,7 +350,7 @@ like(http( 'POST /unbuf HTTP/1.0' . CRLF . 'Content-Length: 10' . CRLF . CRLF . '0', - sleep => 0.1, + sleep => 0.2, body => '123456789' ), qr/ 502 .*backend body:::/s, 'unbuf proxy small'); @@ -366,7 +366,7 @@ like(http( 'Host: localhost' . CRLF . 'Connection: close' . CRLF . 'Transfer-Encoding: chunked' . CRLF . CRLF, - sleep => 0.1, + sleep => 0.2, body => 'a' . CRLF . '0123456789' . CRLF . @@ -380,7 +380,7 @@ like(http( 'Transfer-Encoding: chunked' . CRLF . CRLF . '1' . CRLF . 'X' . CRLF, - sleep => 0.1, + sleep => 0.2, body => '9' . CRLF . '123456789' . CRLF . diff --git a/h2_request_body_discard.t b/h2_request_body_discard.t --- a/h2_request_body_discard.t +++ b/h2_request_body_discard.t @@ -258,11 +258,11 @@ like(http2_get_body_incomplete_nolen('/u # error_page 400 after proxy with request buffering disabled -like(http2_get_body_custom('/unbuf2', 1, '', sleep => 0.1), +like(http2_get_body_custom('/unbuf2', 1, '', sleep => 0.2), qr/status: 400.*backend body:::/s, 'unbuf too short'); -like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.1), +like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.2), qr/status: 400.*backend body:::/s, 'unbuf too long'); -like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.1, more => 1), +like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.2, more => 1), qr/status: 400.*backend body:::/s, 'unbuf too long more'); # error_page 413 and $content_length diff --git a/h3_request_body_discard.t b/h3_request_body_discard.t --- a/h3_request_body_discard.t +++ b/h3_request_body_discard.t @@ -258,7 +258,7 @@ like(http3_get_body_custom('/proxy', 1, like(http3_get('/unbuf'), qr/status: 502.*backend body:::/s, 'unbuf proxy'); -like(http3_get_body_custom('/unbuf', 10, '0123456789', sleep => 0.1), +like(http3_get_body_custom('/unbuf', 10, '0123456789', sleep => 0.2), qr/status: 502.*backend body:::/s, 'unbuf proxy small'); like(http3_get_body_incomplete('/unbuf', 10000, '0123456789'), qr/status: 502.*backend body:::/s, 'unbuf proxy long'); @@ -271,11 +271,11 @@ like(http3_get_body_incomplete_nolen('/u # error_page 400 after proxy with request buffering disabled -like(http3_get_body_custom('/unbuf2', 1, '', sleep => 0.1), +like(http3_get_body_custom('/unbuf2', 1, '', sleep => 0.2), qr/status: 400.*backend body:::/s, 'unbuf too short'); -like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.1), +like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.2), qr/status: 400.*backend body:::/s, 'unbuf too long'); -like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.1, more => 1), +like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.2, more => 1), qr/status: 400.*backend body:::/s, 'unbuf too long more'); # error_page 413 and $content_length @@ -333,12 +333,12 @@ sub http3_get_body_nolen { my $sid = $s->new_stream({ path => $uri, body_more => 1 }); if (defined $body2) { - select undef, undef, undef, 0.1; + select undef, undef, undef, 0.2; $s->h3_body($body, $sid, { body_more => 1 }); - select undef, undef, undef, 0.1; + select undef, undef, undef, 0.2; $s->h3_body($body2, $sid); } else { - select undef, undef, undef, 0.1; + select undef, undef, undef, 0.2; $s->h3_body($body, $sid); } From mdounin at mdounin.ru Thu Aug 21 01:33:01 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Thu, 21 Aug 2025 04:33:01 +0300 Subject: [nginx-tests] Tests: adjusted delays in body discard tests. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/250fb78dd270 branches: changeset: 2020:250fb78dd270 user: Maxim Dounin date: Tue Aug 19 03:25:52 2025 +0300 description: Tests: adjusted delays in body discard tests. At least h3_request_body_discard.t occasionally fails on slow hosts due to too short delays, and using longer delays fixes this. diffstat: body_discard.t | 6 +++--- h2_request_body_discard.t | 6 +++--- h3_request_body_discard.t | 14 +++++++------- 3 files changed, 13 insertions(+), 13 deletions(-) diffs (91 lines): diff --git a/body_discard.t b/body_discard.t --- a/body_discard.t +++ b/body_discard.t @@ -350,7 +350,7 @@ like(http( 'POST /unbuf HTTP/1.0' . CRLF . 'Content-Length: 10' . CRLF . CRLF . '0', - sleep => 0.1, + sleep => 0.2, body => '123456789' ), qr/ 502 .*backend body:::/s, 'unbuf proxy small'); @@ -366,7 +366,7 @@ like(http( 'Host: localhost' . CRLF . 'Connection: close' . CRLF . 'Transfer-Encoding: chunked' . CRLF . CRLF, - sleep => 0.1, + sleep => 0.2, body => 'a' . CRLF . '0123456789' . CRLF . @@ -380,7 +380,7 @@ like(http( 'Transfer-Encoding: chunked' . CRLF . CRLF . '1' . CRLF . 'X' . CRLF, - sleep => 0.1, + sleep => 0.2, body => '9' . CRLF . '123456789' . CRLF . diff --git a/h2_request_body_discard.t b/h2_request_body_discard.t --- a/h2_request_body_discard.t +++ b/h2_request_body_discard.t @@ -258,11 +258,11 @@ like(http2_get_body_incomplete_nolen('/u # error_page 400 after proxy with request buffering disabled -like(http2_get_body_custom('/unbuf2', 1, '', sleep => 0.1), +like(http2_get_body_custom('/unbuf2', 1, '', sleep => 0.2), qr/status: 400.*backend body:::/s, 'unbuf too short'); -like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.1), +like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.2), qr/status: 400.*backend body:::/s, 'unbuf too long'); -like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.1, more => 1), +like(http2_get_body_custom('/unbuf2', 1, '01', sleep => 0.2, more => 1), qr/status: 400.*backend body:::/s, 'unbuf too long more'); # error_page 413 and $content_length diff --git a/h3_request_body_discard.t b/h3_request_body_discard.t --- a/h3_request_body_discard.t +++ b/h3_request_body_discard.t @@ -258,7 +258,7 @@ like(http3_get_body_custom('/proxy', 1, like(http3_get('/unbuf'), qr/status: 502.*backend body:::/s, 'unbuf proxy'); -like(http3_get_body_custom('/unbuf', 10, '0123456789', sleep => 0.1), +like(http3_get_body_custom('/unbuf', 10, '0123456789', sleep => 0.2), qr/status: 502.*backend body:::/s, 'unbuf proxy small'); like(http3_get_body_incomplete('/unbuf', 10000, '0123456789'), qr/status: 502.*backend body:::/s, 'unbuf proxy long'); @@ -271,11 +271,11 @@ like(http3_get_body_incomplete_nolen('/u # error_page 400 after proxy with request buffering disabled -like(http3_get_body_custom('/unbuf2', 1, '', sleep => 0.1), +like(http3_get_body_custom('/unbuf2', 1, '', sleep => 0.2), qr/status: 400.*backend body:::/s, 'unbuf too short'); -like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.1), +like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.2), qr/status: 400.*backend body:::/s, 'unbuf too long'); -like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.1, more => 1), +like(http3_get_body_custom('/unbuf2', 1, '01', sleep => 0.2, more => 1), qr/status: 400.*backend body:::/s, 'unbuf too long more'); # error_page 413 and $content_length @@ -333,12 +333,12 @@ sub http3_get_body_nolen { my $sid = $s->new_stream({ path => $uri, body_more => 1 }); if (defined $body2) { - select undef, undef, undef, 0.1; + select undef, undef, undef, 0.2; $s->h3_body($body, $sid, { body_more => 1 }); - select undef, undef, undef, 0.1; + select undef, undef, undef, 0.2; $s->h3_body($body2, $sid); } else { - select undef, undef, undef, 0.1; + select undef, undef, undef, 0.2; $s->h3_body($body, $sid); } From mdounin at mdounin.ru Thu Aug 21 02:29:27 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Thu, 21 Aug 2025 05:29:27 +0300 Subject: [nginx] SSL: fixed subjectAltName and commonName debug logging. Message-ID: details: http://freenginx.org/hg/nginx/rev/a540fac7e04a branches: changeset: 9410:a540fac7e04a user: Maxim Dounin date: Thu Aug 21 04:40:59 2025 +0300 description: 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". diffstat: src/event/ngx_event_openssl.c | 6 ++++-- 1 files changed, 4 insertions(+), 2 deletions(-) diffs (23 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 @@ -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 21 02:29:27 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Thu, 21 Aug 2025 05:29:27 +0300 Subject: [nginx] SSL: support for iPAddress subjectAltName in certificates. Message-ID: details: http://freenginx.org/hg/nginx/rev/5e23335edf67 branches: changeset: 9411:5e23335edf67 user: Maxim Dounin date: Thu Aug 21 04:41:27 2025 +0300 description: 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 diffstat: src/event/ngx_event_openssl.c | 126 +++++++++++++++++++++++++++++++++++------ 1 files changed, 106 insertions(+), 20 deletions(-) diffs (165 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 @@ -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_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; + } + + } else 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; + } } } From mdounin at mdounin.ru Thu Aug 21 02:29:50 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Thu, 21 Aug 2025 05:29:50 +0300 Subject: [nginx-tests] Tests: iPAddress subjectAltName tests. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/1bf186036193 branches: changeset: 2021:1bf186036193 user: Maxim Dounin date: Thu Aug 21 04:46:24 2025 +0300 description: Tests: iPAddress subjectAltName tests. diffstat: proxy_ssl_verify.t | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 files changed, 42 insertions(+), 2 deletions(-) diffs (73 lines): 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 21 17:08:51 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Thu, 21 Aug 2025 20:08:51 +0300 Subject: [nginx] Mail: s->login and s->passwd now cleared on errors. Message-ID: details: http://freenginx.org/hg/nginx/rev/a6d2f13a8539 branches: changeset: 9412:a6d2f13a8539 user: Maxim Dounin date: Thu Aug 21 06:02:01 2025 +0300 description: 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). diffstat: src/mail/ngx_mail_auth_http_module.c | 2 ++ src/mail/ngx_mail_handler.c | 4 ++++ src/mail/ngx_mail_imap_handler.c | 4 +++- src/mail/ngx_mail_pop3_handler.c | 2 ++ src/mail/ngx_mail_smtp_handler.c | 2 ++ 5 files changed, 13 insertions(+), 1 deletions(-) diffs (73 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 @@ -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 21 17:08:51 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Thu, 21 Aug 2025 20:08:51 +0300 Subject: [nginx] Mail: fixed "upstream: ..." in logs with "smtp_auth none". Message-ID: details: http://freenginx.org/hg/nginx/rev/983ba8e0d396 branches: changeset: 9413:983ba8e0d396 user: Maxim Dounin date: Thu Aug 21 06:02:08 2025 +0300 description: 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(). diffstat: src/mail/ngx_mail_handler.c | 16 ++++++---------- 1 files changed, 6 insertions(+), 10 deletions(-) diffs (29 lines): 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; } From mdounin at mdounin.ru Thu Aug 21 23:34:37 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 22 Aug 2025 02:34:37 +0300 Subject: [nginx] Updated request line parsing to allow IPv6 zone identifi... Message-ID: details: http://freenginx.org/hg/nginx/rev/ed87641eff8d branches: changeset: 9414:ed87641eff8d user: Maxim Dounin date: Thu Aug 21 23:49:38 2025 +0300 description: Updated request line parsing to allow IPv6 zone identifiers. RFC 6874 defines syntax to allow IPv6 zone identifiers in URIs, by providing an additional option in IP-literal: IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture ) "]" ZoneID = 1*( unreserved / pct-encoded ) IPv6addrz = IPv6address "%25" ZoneID The "%" character wasn't previously allowed in IP literals, and therefore attempts to use such addresses in request line resulted in 400 (Bad Request) (but was accepted in the Host header field, which uses more relaxed parsing). With this change, "%" is now allowed in IP literals. diffstat: src/http/ngx_http_parse.c | 3 +++ 1 files changed, 3 insertions(+), 0 deletions(-) diffs (13 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 @@ -466,6 +466,9 @@ ngx_http_parse_request_line(ngx_http_req case '=': /* sub-delims */ break; + case '%': + /* pct-encoded */ + break; default: return NGX_HTTP_PARSE_INVALID_REQUEST; } From mdounin at mdounin.ru Thu Aug 21 23:34:37 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 22 Aug 2025 02:34:37 +0300 Subject: [nginx] Updated request line parsing to allow uncommon chars in ... Message-ID: details: http://freenginx.org/hg/nginx/rev/c5503ee3c658 branches: changeset: 9415:c5503ee3c658 user: Maxim Dounin date: Thu Aug 21 23:49:41 2025 +0300 description: Updated request line parsing to allow uncommon chars in host. Previously, only ALPHA, DIGIT, ".", and "-" were allowed in the host component of the request line (if it's not an IP literal). On the other hand, RFC 3986 allows the following: reg-name = *( unreserved / pct-encoded / sub-delims ) unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" pct-encoded = "%" HEXDIG HEXDIG Notably, the "_" character is used in practice but was not allowed in the request line. At the same time, this and other characters do actually work in practice, as they are accepted in the Host header field, which uses more relaxed parsing. With this change, all characters which are valid in the host name per RFC 3986 are also allowed in the request line. diffstat: src/http/ngx_http_parse.c | 11 ++++++++++- 1 files changed, 10 insertions(+), 1 deletions(-) diffs (21 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 @@ -392,7 +392,16 @@ ngx_http_parse_request_line(ngx_http_req break; } - if ((ch >= '0' && ch <= '9') || ch == '.' || ch == '-') { + if (ch >= '0' && ch <= '9') { + break; + } + + if (ch == '.' || ch == '-' || ch == '_' || ch == '~' + || ch == '!' || ch == '$' || ch == '&' || ch == '\'' + || ch == '(' || ch == ')' || ch == '*' || ch == '+' + || ch == ',' || ch == ';' || ch == '=' || ch == '%') + { + /* unreserved, sub-delims, pct-encoded */ break; } From mdounin at mdounin.ru Thu Aug 21 23:34:37 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 22 Aug 2025 02:34:37 +0300 Subject: [nginx] Updated request line parsing to use if() in IP literals. Message-ID: details: http://freenginx.org/hg/nginx/rev/865956fd7ae6 branches: changeset: 9416:865956fd7ae6 user: Maxim Dounin date: Thu Aug 21 23:49:43 2025 +0300 description: Updated request line parsing to use if() in IP literals. Using if() results in more readable code and matches the code used in host parsing, where it is not convenient to use switch(). Besides, it is slightly faster than switch() on typical inputs. diffstat: src/http/ngx_http_parse.c | 37 +++++++++++++------------------------ 1 files changed, 13 insertions(+), 24 deletions(-) diffs (51 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 @@ -450,34 +450,23 @@ ngx_http_parse_request_line(ngx_http_req break; } + if (ch == ':') { + break; + } + + if (ch == '.' || ch == '-' || ch == '_' || ch == '~' + || ch == '!' || ch == '$' || ch == '&' || ch == '\'' + || ch == '(' || ch == ')' || ch == '*' || ch == '+' + || ch == ',' || ch == ';' || ch == '=' || ch == '%') + { + /* unreserved, sub-delims, pct-encoded */ + break; + } + switch (ch) { - case ':': - break; case ']': state = sw_host_end; break; - case '-': - case '.': - case '_': - case '~': - /* unreserved */ - break; - case '!': - case '$': - case '&': - case '\'': - case '(': - case ')': - case '*': - case '+': - case ',': - case ';': - case '=': - /* sub-delims */ - break; - case '%': - /* pct-encoded */ - break; default: return NGX_HTTP_PARSE_INVALID_REQUEST; } From mdounin at mdounin.ru Thu Aug 21 23:34:37 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 22 Aug 2025 02:34:37 +0300 Subject: [nginx] Improved ngx_http_validate_host() to do better syntax va... Message-ID: details: http://freenginx.org/hg/nginx/rev/4ae00e4104c2 branches: changeset: 9417:4ae00e4104c2 user: Maxim Dounin date: Thu Aug 21 23:49:46 2025 +0300 description: Improved ngx_http_validate_host() to do better syntax validation. With this change, syntax validation in ngx_http_validate_host() is mostly identical to the one used for the request line parsing, which now follows RFC 3986. In particular, as a result of this change non-ASCII characters and invalid port numbers are no longer allowed in the Host header. This also fixes "." not being stripped if port contained ".", as such requests are now rejected by syntax checks. Prodded by Anton Mironenko, https://github.com/freenginx/nginx/issues/11 diffstat: src/http/ngx_http_request.c | 151 ++++++++++++++++++++++++++++++++----------- 1 files changed, 112 insertions(+), 39 deletions(-) diffs (181 lines): 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 @@ -2155,9 +2155,11 @@ ngx_http_validate_host(ngx_str_t *host, size_t i, dot_pos, host_len; enum { - sw_usual = 0, + sw_start = 0, + sw_host, + sw_host_end, sw_literal, - sw_rest + sw_port } state; dot_pos = host->len; @@ -2165,55 +2167,126 @@ ngx_http_validate_host(ngx_str_t *host, h = host->data; - state = sw_usual; + state = sw_start; for (i = 0; i < host->len; i++) { ch = h[i]; - switch (ch) { - - case '.': - if (dot_pos == i - 1) { - return NGX_DECLINED; - } - dot_pos = i; - break; - - case ':': - if (state == sw_usual) { - host_len = i; - state = sw_rest; - } - break; - - case '[': - if (i == 0) { + switch (state) { + + case sw_start: + + if (ch == '[') { + host_len = 0; state = sw_literal; - } - break; - - case ']': - if (state == sw_literal) { - host_len = i + 1; - state = sw_rest; + break; } - break; - - default: - - if (ngx_path_separator(ch)) { - return NGX_DECLINED; - } - - if (ch <= 0x20 || ch == 0x7f) { - return NGX_DECLINED; + + state = sw_host; + + /* fall through */ + + case sw_host: + + if (ch >= 'a' && ch <= 'z') { + break; } if (ch >= 'A' && ch <= 'Z') { alloc = 1; + break; } - break; + if (ch >= '0' && ch <= '9') { + break; + } + + if (ch == '.') { + if (dot_pos == i - 1) { + return NGX_DECLINED; + } + dot_pos = i; + break; + } + + if (ch == '-' || ch == '_' || ch == '~' + || ch == '!' || ch == '$' || ch == '&' || ch == '\'' + || ch == '(' || ch == ')' || ch == '*' || ch == '+' + || ch == ',' || ch == ';' || ch == '=' || ch == '%') + { + /* unreserved, sub-delims, pct-encoded */ + break; + } + + /* fall through */ + + case sw_host_end: + + host_len = i; + + if (ch == ':') { + state = sw_port; + break; + } + + /* notably, "/" and "\" are rejected */ + + return NGX_DECLINED; + + case sw_literal: + + if (ch >= '0' && ch <= '9') { + break; + } + + if (ch >= 'a' && ch <= 'z') { + break; + } + + if (ch >= 'A' && ch <= 'Z') { + alloc = 1; + break; + } + + if (ch == ':') { + break; + } + + if (ch == '.') { + if (dot_pos == i - 1) { + return NGX_DECLINED; + } + dot_pos = i; + break; + } + + if (ch == '-' || ch == '_' || ch == '~' + || ch == '!' || ch == '$' || ch == '&' || ch == '\'' + || ch == '(' || ch == ')' || ch == '*' || ch == '+' + || ch == ',' || ch == ';' || ch == '=' || ch == '%') + { + /* unreserved, sub-delims, pct-encoded */ + break; + } + + if (ch == ']') { + host_len = i + 1; + state = sw_host_end; + break; + } + + /* notably, "/" and "\" are rejected */ + + return NGX_DECLINED; + + case sw_port: + + if (ch >= '0' && ch <= '9') { + break; + } + + return NGX_DECLINED; + } } From mdounin at mdounin.ru Thu Aug 21 23:34:52 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Fri, 22 Aug 2025 02:34:52 +0300 Subject: [nginx-tests] Tests: rewritten http_host.t. Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/aeae7e1c9f00 branches: changeset: 2022:aeae7e1c9f00 user: Maxim Dounin date: Thu Aug 21 23:50:09 2025 +0300 description: Tests: rewritten http_host.t. Resulting checks are expected to be more consistent, and test for all possible cases via the Host header and via the request line (as long as it is possible). Tests now also cover recent changes to ngx_http_parse_request_line() (more relaxed parsing, matching RFC 3986) and in the Host header (stricter parsing, matching RFC 3986). diffstat: http_host.t | 330 +++++++++++++++++++++++++++++++++++++++++------------------ 1 files changed, 228 insertions(+), 102 deletions(-) diffs (386 lines): diff --git a/http_host.t b/http_host.t --- a/http_host.t +++ b/http_host.t @@ -22,7 +22,7 @@ use Test::Nginx qw/ :DEFAULT http_conten select STDERR; $| = 1; select STDOUT; $| = 1; -my $t = Test::Nginx->new()->has(qw/http rewrite/)->plan(37); +my $t = Test::Nginx->new()->has(qw/http rewrite/)->plan(74); $t->write_file_expand('nginx.conf', <<'EOF'); @@ -52,146 +52,272 @@ EOF ############################################################################### -is(http_host_header('www.abcd-ef.g02.xyz'), 'www.abcd-ef.g02.xyz', - 'domain w/o port (host header)'); -is(http_host_header('abcd-ef.g02.xyz:' . port(8080)), 'abcd-ef.g02.xyz', - 'domain w/port (host header)'); +# host +# host:port +# host. +# host.:port +# HOST +# HOST:port +# host09 +# ho-st +# _host + +is(hh('example.com'), 'example.com', 'host'); +is(rl('example.com'), 'example.com', 'host in request line'); -is(http_absolute_path('abcd-ef.g02.xyz'), 'abcd-ef.g02.xyz', - 'domain w/o port (absolute request)'); -is(http_absolute_path('www.abcd-ef.g02.xyz:10'), 'www.abcd-ef.g02.xyz', - 'domain w/port (absolute request)'); +is(hh('example.com:80'), 'example.com', 'host:port'); +is(rl('example.com:80'), 'example.com', 'host:port in request line'); + +is(hh('example.com.'), 'example.com', 'host with dot'); +is(rl('example.com.'), 'example.com', 'host with dot in request line'); +is(hh('example.com.:80'), 'example.com', 'host:port with dot'); +is(rl('example.com.:80'), 'example.com', 'host:port with dot in request line'); -is(http_host_header('www.abcd-ef.g02.xyz.'), 'www.abcd-ef.g02.xyz', - 'domain w/ ending dot w/o port (host header)'); +is(hh('EXAMPLE.com'), 'example.com', 'host with uppercase'); +is(rl('EXAMPLE.com'), 'example.com', 'host with uppercase in request line'); + +is(hh('EXAMPLE.com:80'), 'example.com', 'host:port with uppercase'); +is(rl('EXAMPLE.com:80'), 'example.com', + 'host:port with uppercase in request line'); -is(http_host_header('abcd-ef.g02.xyz.:88'), 'abcd-ef.g02.xyz', - 'domain w/ ending dot w/port (host header)'); +is(hh('foo09.example.com'), 'foo09.example.com', 'host with digits'); +is(rl('foo09.example.com'), 'foo09.example.com', + 'host with digits in request line'); + +is(hh('foo-bar.example.com'), 'foo-bar.example.com', 'host with dash'); +is(rl('foo-bar.example.com'), 'foo-bar.example.com', + 'host with dash in request line'); + +is(hh('_foo.example.com'), '_foo.example.com', 'host with underscore'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +is(rl('_foo.example.com'), '_foo.example.com', + 'host with underscore in request line'); -is(http_absolute_path('www.abcd-ef.g02.xyz.'), 'www.abcd-ef.g02.xyz', - 'domain w/ ending dot w/o port (absolute request)'); -is(http_absolute_path('abcd-ef.g02.xyz.:2'), 'abcd-ef.g02.xyz', - 'domain w/ ending dot w/port (absolute request)'); +} +# all characters permitted by RFC 3986 +# (unreserved, pct-encoded, sub-delims) + +is(hh(q{-._~!$&'()*+,;=%25.example.com}), q{-._~!$&'()*+,;=%25.example.com}, + 'host with sub-delims'); -is(http_absolute_path('AbC-d93.0.34ZhGt-s.nk.Ru'), 'abc-d93.0.34zhgt-s.nk.ru', - 'mixed case domain w/o port (absolute request)'); -is(http_host_header('AbC-d93.0.34ZhGt-s.nk.Ru:88'), 'abc-d93.0.34zhgt-s.nk.ru', - 'mixed case domain w/port (host header)'); +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); +is(rl(q{-._~!$&'()*+,;=%25.example.com}), q{-._~!$&'()*+,;=%25.example.com}, + 'host with sub-delims in request line'); + +} -is(http_host_header('123.40.56.78'), '123.40.56.78', - 'ipv4 w/o port (host header)'); -is(http_host_header('123.49.0.78:987'), '123.49.0.78', - 'ipv4 w/port (host header)'); +# ip +# ip:port +# ipv6 +# ipv6:port +# ipv6-v4mapped +# ipv6-v4mapped:port + +is(hh('192.0.2.1'), '192.0.2.1', 'ip'); +is(rl('192.0.2.1'), '192.0.2.1', 'ip in request line'); + +is(hh('192.0.2.1:80'), '192.0.2.1', 'ip'); +is(rl('192.0.2.1:80'), '192.0.2.1', 'ip:port in request line'); -is(http_absolute_path('123.49.0.78'), '123.49.0.78', - 'ipv4 w/o port (absolute request)'); -is(http_absolute_path('123.40.56.78:123'), '123.40.56.78', - 'ipv4 w/port (absolute request)'); +is(hh('[2001:db8::1]'), '[2001:db8::1]', 'ipv6'); +is(rl('[2001:db8::1]'), '[2001:db8::1]', 'ipv6 in request line'); + +is(hh('[2001:db8::1]:80'), '[2001:db8::1]', 'ipv6:port'); +is(rl('[2001:db8::1]:80'), '[2001:db8::1]', 'ipv6:port in request line'); -is(http_host_header('[abcd::ef98:0:7654:321]'), '[abcd::ef98:0:7654:321]', - 'ipv6 literal w/o port (host header)'); -is(http_host_header('[abcd::ef98:0:7654:321]:80'), '[abcd::ef98:0:7654:321]', - 'ipv6 literal w/port (host header)'); +is(hh('[2001:DB8::1]'), '[2001:db8::1]', 'ipv6 with uppercase'); +is(rl('[2001:DB8::1]'), '[2001:db8::1]', + 'ipv6 with uppercase in request line'); + +is(hh('[2001:DB8::1]:80'), '[2001:db8::1]', 'ipv6:port with uppercase'); +is(rl('[2001:DB8::1]:80'), '[2001:db8::1]', + 'ipv6:port with uppercase in request line'); -is(http_absolute_path('[abcd::ef98:0:7654:321]'), '[abcd::ef98:0:7654:321]', - 'ipv6 literal w/o port (absolute request)'); -is(http_absolute_path('[abcd::ef98:0:7654:321]:5'), '[abcd::ef98:0:7654:321]', - 'ipv6 literal w/port (absolute request)'); +is(hh('[::ffff:192.0.2.1]'), '[::ffff:192.0.2.1]', 'ipv6 v4mapped'); +is(rl('[::ffff:192.0.2.1]'), '[::ffff:192.0.2.1]', + 'ipv6 v4mapped in request line'); + +is(hh('[::ffff:192.0.2.1]:80'), '[::ffff:192.0.2.1]', 'ipv6:port v4mapped '); +is(rl('[::ffff:192.0.2.1]:80'), '[::ffff:192.0.2.1]', + 'ipv6:port v4mapped in request line'); + +# ipv6 with zoneid, RFC 6874 + +is(hh('[2001:db8::1%25en1]'), '[2001:db8::1%25en1]', 'ipv6 zoneid'); +is(hh('[2001:db8::1%25en1]:80'), '[2001:db8::1%25en1]', 'ipv6:port zoneid'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); -is(http_host_header('[::ffff:12.30.67.89]'), '[::ffff:12.30.67.89]', - 'ipv4-mapped ipv6 w/o port (host header)'); -is(http_host_header('[::123.45.67.89]:4321'), '[::123.45.67.89]', - 'ipv4-mapped ipv6 w/port (host header)'); +is(rl('[2001:db8::1%25en1]'), '[2001:db8::1%25en1]', + 'ipv6 zoneid in request line'); +is(rl('[2001:db8::1%25en1]:80'), '[2001:db8::1%25en1]', + 'ipv6:port zoneid in request line'); +} + +# ipvfuture -is(http_absolute_path('[::123.45.67.89]'), '[::123.45.67.89]', - 'ipv4-mapped ipv6 w/o port (absolute request)'); -is(http_absolute_path('[::ffff:12.30.67.89]:4321'), '[::ffff:12.30.67.89]', - 'ipv4-mapped ipv6 w/port (absolute request)'); +is(hh('[v0.1azAZ.!$&\'()*+,;=-._~:]'), '[v0.1azaz.!$&\'()*+,;=-._~:]', + 'ipvfuture'); +is(rl('[v0.1azAZ.!$&\'()*+,;=-._~:]'), '[v0.1azaz.!$&\'()*+,;=-._~:]', + 'ipvfuture in request line'); + +is(hh('[v0.1azAZ.!$&\'()*+,;=-._~:]:80'), '[v0.1azaz.!$&\'()*+,;=-._~:]', + 'ipvfuture:port'); +is(rl('[v0.1azAZ.!$&\'()*+,;=-._~:]:80'), '[v0.1azaz.!$&\'()*+,;=-._~:]', + 'ipvfuture:port in request line'); -like(http_host_header('example.com/\:552', 1), qr/ 400 /, - 'domain w/ path separators (host header)'); -like(http_absolute_path('\e/xample.com', 1), qr/ 400 /, - 'domain w/ path separators (absolute request)'); +# various invalid cases: +# +# example/com (only make sense in host header) +# example\com +# example..com +# example.com:port:port +# example.com:invalid_port +# [ipv6/foo] +# [ipvfuture/foo] +# [ipv6..foo] +# [ipvfuture..foo] +# [ipv6 (no closing "]") +# [ipvfuture (no closing "]") -like(http_host_header('..examp-LE.com', 1), qr/ 400 /, - 'domain w/ double dot (host header)'); -like(http_absolute_path('com.exa-m.45..:', 1), qr/ 400 /, - 'domain w/ double dot (absolute request)'); +like(hh('example/com'), qr/ 400 /, 'host with slash'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1') + or $^O eq 'MSWin32'; +like(hh('example\com'), qr/ 400 /, 'host with backslash'); + +} + +like(rl('example\com'), qr/ 400 /, 'host with backslash in request line'); + +like(hh('example..com'), qr/ 400 /, 'host with double dots'); +like(rl('example..com'), qr/ 400 /, 'host with double dots in request line'); -like(http_host_header('[abcd::e\f98:0/:7654:321]', 1), qr/ 400 /, - 'ipv6 literal w/ path separators (host header)'); -like(http_absolute_path('[abcd\::ef98:0:7654:321/]:12', 1), qr/ 400 /, - 'ipv6 literal w/ path separators (absolute request)'); +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +like(hh('example.com:80:80'), qr/ 400 /, 'host with two ports'); + +} + +like(rl('example.com:80:80'), qr/ 400 /, + 'host with two ports in request line'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +like(hh('example.com:foo'), qr/ 400 /, 'host with invalid port'); -like(http_host_header('[abcd::ef98:0:7654:321]..:98', 1), qr/ 400 /, - 'ipv6 literal w/ double dot (host header)'); -like(http_absolute_path('[ab..cd::ef98:0:7654:321]', 1), qr/ 400 /, - 'ipv6 literal w/ double dot (absolute request)'); +} +like(rl('example.com:foo'), qr/ 400 /, + 'host with invalid port in request line'); -like(http_host_header('[abcd::ef98:0:7654:321]..:98', 1), qr/ 400 /, - 'ipv6 literal w/ double dot (host header)'); -like(http_absolute_path('[ab..cd::ef98:0:7654:321]', 1), qr/ 400 /, - 'ipv6 literal w/ double dot (absolute request)'); +like(hh('[2001:db8::1/2]'), qr/ 400 /, 'ipv6 with slash'); +like(rl('[2001:db8::1/2]'), qr/ 400 /, + 'ipv6 with slash in request line'); +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1') + or $^O eq 'MSWin32'; + +like(hh('[2001:db8::1\2]'), qr/ 400 /, 'ipv6 with backslash'); + +} -# As per RFC 3986, -# http://tools.ietf.org/html/rfc3986#section-3.2.2 -# -# IP-literal = "[" ( IPv6address / IPvFuture ) "]" -# -# IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) -# -# sub-delims = "!" / "$" / "&" / "'" / "(" / ")" -# / "*" / "+" / "," / ";" / "=" -# -# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" -# +like(rl('[2001:db8::1\2]'), qr/ 400 /, 'ipv6 with backslash in request line'); + +like(hh('[2001:db8::1..2]'), qr/ 400 /, 'ipv6 with double dots'); +like(rl('[2001:db8::1..2]'), qr/ 400 /, + 'ipv6 with double dots in request line'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +like(hh('[2001:db8::1'), qr/ 400 /, 'ipv6 without closing bracket'); + +} + +like(rl('[2001:db8::1'), qr/ 400 /, + 'ipv6 without closing bracket in request line'); + +like(hh('[v0.1/2]'), qr/ 400 /, 'ipvfuture with slash'); +like(rl('[v0.1/2]'), qr/ 400 /, 'ipvfuture with slash in request line'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1') + or $^O eq 'MSWin32'; + +like(hh('[v0.1\2]'), qr/ 400 /, 'ipvfuture with backslash'); + +} + +like(rl('[v0.1\2]'), qr/ 400 /, 'ipvfuture with backslash in request line'); + +like(hh('[v0.1..2]'), qr/ 400 /, 'ipvfuture with double dots'); +like(rl('[v0.1..2]'), qr/ 400 /, 'ipvfuture with double dots in request line'); -is(http_host_header( - '[v0123456789aBcDeF.!$&\'()*+,;=-._~AbCdEfGhIjKlMnOpQrStUvWxYz' - . '0123456789:]'), - '[v0123456789abcdef.!$&\'()*+,;=-._~abcdefghijklmnopqrstuvwxyz' - . '0123456789:]', - 'IPvFuture all symbols (host header)'); +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +like(hh('[v0.1'), qr/ 400 /, 'ipvfuture without closing bracket'); + +} + +like(rl('[v0.1'), qr/ 400 /, + 'ipvfuture without closing bracket in request line'); + +# control characters + +like(hh("example.com\x02"), qr/ 400 /, 'host with control chars'); +like(rl("example.com\x02"), qr/ 400 /, + 'host with control chars in request line'); -is(http_absolute_path( - '[v0123456789aBcDeF.!$&\'()*+,;=-._~AbCdEfGhIjKlMnOpQrStUvWxYz' - . '0123456789:]'), - '[v0123456789abcdef.!$&\'()*+,;=-._~abcdefghijklmnopqrstuvwxyz' - . '0123456789:]', - 'IPvFuture all symbols (absolute request)'); +# non-ascii characters + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.29.1'); + +like(hh("example.com\xff"), qr/ 400 /, 'host with non-ascii chars'); -is(http_host_header('123.40.56.78:9000:80'), '123.40.56.78', - 'double port hack'); +} -like(http_host_header("localhost\nHost: again", 1), qr/ 400 /, 'host repeat'); -like(http_host_header("localhost\x02", 1), qr/ 400 /, 'control'); +like(rl("example.com\xff"), qr/ 400 /, + 'host with non-ascii chars in request line'); + +# multiple host headers + +like(hh("localhost\nHost: again"), qr/ 400 /, 'duplicate host'); ############################################################################### -sub http_host_header { - my ($host, $all) = @_; - my ($r) = http(< details: http://freenginx.org/hg/nginx/rev/cfe8cf095ff0 branches: changeset: 9418:cfe8cf095ff0 user: Maxim Dounin date: Sun Aug 24 20:14:28 2025 +0300 description: Proxy: added the "proxy_allow_duplicate_chunked" directive. This directive allows to accept duplicate "Transfer-Encoding: chunked" header lines. These are invalid, and rejected since 8033:2bf7792c262e (1.23.0), yet it turns out there are quite a few homegrown proxies, notably Java-based ones, which emit such duplicate headers. The "proxy_allow_duplicate_chunked" directive makes it possible to enable compatibility with such proxies by ignoring duplicate "Transfer-Encoding: chunked" headers instead of rejecting them. Prodded by Gennady Bekasov, https://github.com/freenginx/nginx/issues/11 diffstat: src/http/modules/ngx_http_fastcgi_module.c | 4 ++-- src/http/modules/ngx_http_grpc_module.c | 1 + src/http/modules/ngx_http_memcached_module.c | 1 + src/http/modules/ngx_http_proxy_module.c | 14 ++++++++++++-- src/http/modules/ngx_http_scgi_module.c | 4 ++-- src/http/modules/ngx_http_uwsgi_module.c | 4 ++-- src/http/ngx_http_upstream.c | 2 +- src/http/ngx_http_upstream.h | 1 + 8 files changed, 22 insertions(+), 9 deletions(-) diffs (139 lines): 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 @@ -2948,10 +2948,10 @@ ngx_http_fastcgi_create_loc_conf(ngx_con conf->upstream.intercept_errors = NGX_CONF_UNSET; - /* "fastcgi_cyclic_temp_file" is disabled */ + /* the hardcoded values */ conf->upstream.cyclic_temp_file = 0; - conf->upstream.change_buffering = 1; + conf->upstream.duplicate_chunked = 0; conf->catch_stderr = NGX_CONF_UNSET_PTR; 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 @@ -4431,6 +4431,7 @@ ngx_http_grpc_create_loc_conf(ngx_conf_t conf->upstream.pass_request_headers = 1; conf->upstream.pass_request_body = 1; conf->upstream.force_ranges = 0; + conf->upstream.duplicate_chunked = 0; conf->upstream.pass_trailers = 1; conf->upstream.preserve_output = 1; 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 @@ -627,6 +627,7 @@ ngx_http_memcached_create_loc_conf(ngx_c conf->upstream.pass_request_headers = 0; conf->upstream.pass_request_body = 0; conf->upstream.force_ranges = 1; + conf->upstream.duplicate_chunked = 0; conf->index = NGX_CONF_UNSET; conf->gzip_flag = NGX_CONF_UNSET_UINT; 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 @@ -695,6 +695,13 @@ static ngx_command_t ngx_http_proxy_com offsetof(ngx_http_proxy_loc_conf_t, http09), NULL }, + { ngx_string("proxy_allow_duplicate_chunked"), + 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, upstream.duplicate_chunked), + NULL }, + #if (NGX_HTTP_SSL) { ngx_string("proxy_ssl_session_reuse"), @@ -3392,6 +3399,7 @@ ngx_http_proxy_create_loc_conf(ngx_conf_ conf->upstream.request_buffering = NGX_CONF_UNSET; conf->upstream.ignore_client_abort = NGX_CONF_UNSET; conf->upstream.force_ranges = NGX_CONF_UNSET; + conf->upstream.duplicate_chunked = NGX_CONF_UNSET; conf->upstream.local = NGX_CONF_UNSET_PTR; conf->upstream.socket_keepalive = NGX_CONF_UNSET; @@ -3444,9 +3452,8 @@ ngx_http_proxy_create_loc_conf(ngx_conf_ conf->ssl_conf_commands = NGX_CONF_UNSET_PTR; #endif - /* "proxy_cyclic_temp_file" is disabled */ + /* the hardcoded values */ conf->upstream.cyclic_temp_file = 0; - conf->upstream.change_buffering = 1; conf->headers_source = NGX_CONF_UNSET_PTR; @@ -3523,6 +3530,9 @@ ngx_http_proxy_merge_loc_conf(ngx_conf_t ngx_conf_merge_value(conf->upstream.force_ranges, prev->upstream.force_ranges, 0); + ngx_conf_merge_value(conf->upstream.duplicate_chunked, + prev->upstream.duplicate_chunked, 0); + ngx_conf_merge_ptr_value(conf->upstream.local, prev->upstream.local, NULL); 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 @@ -1334,10 +1334,10 @@ ngx_http_scgi_create_loc_conf(ngx_conf_t conf->upstream.intercept_errors = NGX_CONF_UNSET; - /* "scgi_cyclic_temp_file" is disabled */ + /* the hardcoded values */ conf->upstream.cyclic_temp_file = 0; - conf->upstream.change_buffering = 1; + conf->upstream.duplicate_chunked = 0; ngx_str_set(&conf->upstream.module, "scgi"); 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 @@ -1578,10 +1578,10 @@ ngx_http_uwsgi_create_loc_conf(ngx_conf_ conf->ssl_conf_commands = NGX_CONF_UNSET_PTR; #endif - /* "uwsgi_cyclic_temp_file" is disabled */ + /* the hardcoded values */ conf->upstream.cyclic_temp_file = 0; - conf->upstream.change_buffering = 1; + conf->upstream.duplicate_chunked = 0; ngx_str_set(&conf->upstream.module, "uwsgi"); 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 @@ -5292,7 +5292,7 @@ ngx_http_upstream_process_transfer_encod u = r->upstream; - if (u->headers_in.transfer_encoding) { + if (u->headers_in.transfer_encoding && !u->conf->duplicate_chunked) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "upstream sent duplicate header line: \"%V: %V\", " "previous value: \"%V: %V\"", 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 @@ -182,6 +182,7 @@ typedef struct { ngx_flag_t intercept_errors; ngx_flag_t cyclic_temp_file; ngx_flag_t force_ranges; + ngx_flag_t duplicate_chunked; ngx_path_t *temp_path; From mdounin at mdounin.ru Sun Aug 24 17:15:27 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Sun, 24 Aug 2025 20:15:27 +0300 Subject: [nginx-tests] Tests: tests for the "proxy_allow_duplicate_chunke... Message-ID: details: http://freenginx.org/hg/nginx-tests/rev/25acbb7405e8 branches: changeset: 2023:25acbb7405e8 user: Maxim Dounin date: Sun Aug 24 20:14:42 2025 +0300 description: Tests: tests for the "proxy_allow_duplicate_chunked" directive. diffstat: proxy_duplicate_chunked.t | 121 ++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 121 insertions(+), 0 deletions(-) diffs (126 lines): diff --git a/proxy_duplicate_chunked.t b/proxy_duplicate_chunked.t new file mode 100644 --- /dev/null +++ b/proxy_duplicate_chunked.t @@ -0,0 +1,121 @@ +#!/usr/bin/perl + +# (C) Maxim Dounin + +# Test for http backend returning response with duplicate "Transfer-Encoding: +# chunked" headers and the "proxy_allow_duplicate_chunked" 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 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; + + location / { + proxy_pass http://127.0.0.1:8081; + proxy_read_timeout 1s; + } + + location /allow/ { + proxy_pass http://127.0.0.1:8081; + proxy_read_timeout 1s; + proxy_allow_duplicate_chunked on; + } + } +} + +EOF + +$t->run_daemon(\&http_daemon); +$t->try_run('no proxy_allow_duplicate_chunked')->plan(3); +$t->waitforsocket('127.0.0.1:' . port(8081)); + +############################################################################### + +like(http_get('/'), qr/200 OK/, 'normal'); + +like(http_get('/duplicate-chunked'), qr/502 Bad/, + 'duplicate transfer encoding'); + +like(http_get('/allow/duplicate-chunked'), qr/200 OK/, + 'duplicate transfer encoding allowed'); + +############################################################################### + +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 . + 'Content-Length: 0' . CRLF . CRLF; + + } elsif ($uri =~ m/duplicate-chunked/) { + + print $client + 'HTTP/1.1 200 OK' . CRLF . + 'Connection: close' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . + 'Transfer-Encoding: chunked' . CRLF . CRLF . + '0' . CRLF . CRLF; + + } + + close $client; + } +} + +############################################################################### From mdounin at mdounin.ru Sun Aug 24 17:28:03 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 24 Aug 2025 20:28:03 +0300 Subject: [PATCH 1 of 2] Documented proxy_allow_http09 Message-ID: # HG changeset patch # User Maxim Dounin # Date 1755905654 -10800 # Sat Aug 23 02:34:14 2025 +0300 # Node ID e9e342ca6ba4bbbd5b7f22dfc097fe06915accf2 # Parent de1ca6140778c58f71cff6bfa6839e9fe3eaf996 Documented proxy_allow_http09. 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="80">
@@ -39,6 +39,21 @@ location / {
+ +on | off +off +http +server +location +1.29.1 + + +Enables support for HTTP/0.9 responses. +By default, such responses are rejected since version 1.29.1. + + + + address 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="80">
@@ -39,6 +39,21 @@ location / {
+ +on | off +off +http +server +location +1.29.1 + + +????????? ????????? ???????, ???????????? ???????? HTTP/0.9. +?? ????????? ????? ?????? ??????????? ??????? ? ?????? 1.29.1. + + + + ????? From mdounin at mdounin.ru Sun Aug 24 17:28:04 2025 From: mdounin at mdounin.ru (=?utf-8?q?Maxim_Dounin?=) Date: Sun, 24 Aug 2025 20:28:04 +0300 Subject: [PATCH 2 of 2] Documented proxy_allow_duplicate_chunked In-Reply-To: References: Message-ID: <851fed1c6352ff058c60.1756056484@vm-bsd.mdounin.ru> # HG changeset patch # User Maxim Dounin # Date 1755905732 -10800 # Sat Aug 23 02:35:32 2025 +0300 # Node ID 851fed1c6352ff058c60058af4fd50e5b73f4c73 # Parent e9e342ca6ba4bbbd5b7f22dfc097fe06915accf2 Documented proxy_allow_duplicate_chunked. 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="81">
@@ -39,6 +39,22 @@ location / {
+ +on | off +off +http +server +location +1.29.1 + + +Enables handling of responses with duplicate +
Transfer-Encoding: chunked
header fields. +By default, such responses are rejected since version 1.23.0. +
+ +
+ on | off off 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="81">
@@ -39,6 +39,22 @@ location / {
+ +on | off +off +http +server +location +1.29.1 + + +????????? ????????? ???????, ?????????? ????????????? +???? ?????????
Transfer-Encoding: chunked
. +?? ????????? ????? ?????? ??????????? ??????? ? ?????? 1.23.0. +
+ +
+ on | off off From lafiel at elven.pw Mon Aug 25 19:30:42 2025 From: lafiel at elven.pw (Lafiel) Date: Mon, 25 Aug 2025 22:30:42 +0300 Subject: [nginx] Update mime-types In-Reply-To: References: <6fd7b4aae9f284157bcd51b2eb936b82@elven.pw> <33cf12d6a7440f5538c60d38ff208870@elven.pw> <25057fabce20e04e31c2db1426b7606f@elven.pw> Message-ID: <13eaac41ace2c393f047d5fd6ec0b10f@elven.pw> Hello! Maxim Dounin ?????(?) 2025-07-28 13:54: > Hello! > > Does it make sense to use these extensions nowadays? > > If yes, how the consumers of these formats will react to the > change - that is, is the "application/vnd.palm" type actually > supported, or it's just a planned change which never happened in > practice? > > Note that Apache uses "application/x-mobipocket-ebook" for "prc" > (which is a prc-based ebook format), which suggests that > "application/vnd.palm" might not work well at least for some > consumers. > > If no, should it be removed instead? > > Also note that at least one common use of the "pdb" extension is a > debug data for Microsoft C++ Compiler, and it's actively used in > practice. I agree, some types are rarely used now. Then maybe it should be removed? There are other rarely used types. Example: $ awk '/^mml,/||/^jad,/||/^wml,/||/^jar,/||/^war,/||/^ear,/||/^hqx,/|| \ /^odg,/||/^odp,/||/^ods,/||/^odt,/||/^wmlc,/||/^wasm,/||/^cco,/|| \ /^jardiff,/||/^jnlp,/||/^sea,/||/^sit,/||/^xpi,/||/^xspf,/|| \ /^ra,/||/^3gp,/||/^asf,/' ~/httparchive_exts.csv 3gp,35,75 wml,11,63 odt,16,23 jar,5,12 wasm,5,9 sea,2,8 xspf,4,7 asf,4,6 odg,2,3 ra,3,3 war,1,1 jnlp,1,1 cco,1,1 hqx,1,1 ods,1,1 > Numbers as I see them suggests that all these extensions are > rather not used in practice, at least compared to "ts". > > Further, I would argue that "tsv" is a common extension for tab > separated values format, and both Python and Apache map it to > "text/tab-separated-values". > > I would rather refrain from this change. Maybe then just add the text/tab-separated-values type for .tsv files? $ awk '/^tsv,/' httparchive_exts.csv tsv,174,326 > See here about m4v/m4a as discussed earlier in this thread: > > https://freenginx.org/pipermail/nginx-devel/2024-April/000141.html > > Note well that this patch doesn't remove corresponding > "video/x-m4v" type with the "m4v" extension, which makes the > change essentially a nop (apart from producing a warning at > startup). Forgot to delete video/x-m4v. If there were any problems, then Apache would not have added the `audio/mp4` type for .m4a and mp4a files. > The numbers suggests that these extensions are not used in > practice. > > And at least "movie" is a way too generic extension seen being > used with other MIME types, notably "video/x-sgi-movie" in Apache > and Python. > > We probably can consider adding just the "qt", but given the > numbers I'm not sure it worth the effort. Apache also has such an extension, I think it's worth adding. I can fix the patch later. $ awk '/video\/quicktime/' mime.types video/quicktime qt mov > At least Apache still uses "video/x-ms-asf". And the change > looses details about the purpose of the file format, which might > be the reason why "video/x-ms-asf" is still used. > > Any specific reasons for the change? According to this recommendation: https://learn.microsoft.com/en-us/windows/win32/wmformat/file-name-extension-guidelines asf files should not store audio and video streams. This format is also rarely used, can it be removed as well? This also applies to the .wmv extension. Then there will be no need to add the .wma extension. > Apache still uses "video/x-msvideo", probably due to the issues > mentioned above. > > Any specific reasons for the change? There are no specific reasons -- Best regards, Lafiel mailto:lafiel at elven.pw -------------- next part -------------- A non-text attachment was scrubbed... Name: 0xFAB0C3D2.asc Type: application/pgp-keys Size: 1839 bytes Desc: not available URL: -------------- next part -------------- A non-text attachment was scrubbed... Name: signature.asc Type: application/pgp-signature Size: 228 bytes Desc: OpenPGP digital signature URL: From mdounin at mdounin.ru Tue Aug 26 00:02:31 2025 From: mdounin at mdounin.ru (Maxim Dounin) Date: Tue, 26 Aug 2025 03:02:31 +0300 Subject: freenginx-1.29.1 changes draft Message-ID: Hello! Below is freenginx-1.29.1 changes draft, comments appreciated. Changes with freenginx 1.29.1 26 Aug 2025 *) Change: now responses from proxied servers over HTTP/0.9 are rejected as invalid by default; the "proxy_allow_http09" directive allows processing of such responses. *) Change: stricter syntax checks are now applied to the "Host" request header line. *) Feature: now interim 1xx responses of proxied servers are ignored. *) Feature: certificates for IP addresses are now supported when verifying backend SSL certificates. *) Feature: the "proxy_allow_duplicate_chunked" directive. Thanks to Gennady Bekasov. *) Bugfix: when using a host name in the request line, the "_" and some other characters were not allowed, as well as zone identifiers in IPv6 addresses. *) Workaround: "shutdown() failed (22: Invalid argument)" alerts might appear in logs on NetBSD. *) Bugfix: in the mail proxy module. ????????? ? freenginx 1.29.1 26.08.2025 *) ?????????: ?????? ?????? ???????????? ???????? ?? HTTP/0.9 ?? ????????? ??????????? ??? ?????????; ????????? proxy_allow_http09 ????????? ?? ?????????. *) ?????????: ? ?????? "Host" ????????? ??????? ?????? ??????????? ????? ??????? ???????? ??????????. *) ??????????: ?????? ????????????? 1xx ?????? ???????????? ???????? ????????????. *) ??????????: ??? ???????? SSL-???????????? ???????? ?????? ?????????????? ??????????? ?? IP-??????. *) ??????????: ????????? proxy_allow_duplicate_chunked. ?????? ???????? ????????. *) ???????????: ??? ????????????? ????? ????? ? ?????? ??????? ?????? "_" ? ????????? ?????? ????????? ?????????????, ? ? IPv6-??????? ?? ??????????? ????????????? ??????????????? ???. *) ?????????: ?? NetBSD ? ????? ????? ?????????? ????????? "shutdown() failed (22: Invalid argument)". *) ???????????: ? ???????? ??????-???????. -- Maxim Dounin http://mdounin.ru/ From mdounin at mdounin.ru Tue Aug 26 15:07:11 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 26 Aug 2025 18:07:11 +0300 Subject: [nginx-site] Documented proxy_allow_http09. Message-ID: details: http://freenginx.org/hg/nginx-site/rev/e9e342ca6ba4 branches: changeset: 3115:e9e342ca6ba4 user: Maxim Dounin date: Sat Aug 23 02:34:14 2025 +0300 description: Documented proxy_allow_http09. diffstat: xml/en/docs/http/ngx_http_proxy_module.xml | 17 ++++++++++++++++- xml/ru/docs/http/ngx_http_proxy_module.xml | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diffs (68 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="80">
@@ -39,6 +39,21 @@ location / {
+ +on | off +off +http +server +location +1.29.1 + + +Enables support for HTTP/0.9 responses. +By default, such responses are rejected since version 1.29.1. + + + + address 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="80">
@@ -39,6 +39,21 @@ location / {
+ +on | off +off +http +server +location +1.29.1 + + +????????? ????????? ???????, ???????????? ???????? HTTP/0.9. +?? ????????? ????? ?????? ??????????? ??????? ? ?????? 1.29.1. + + + + ????? From mdounin at mdounin.ru Tue Aug 26 15:07:11 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 26 Aug 2025 18:07:11 +0300 Subject: [nginx-site] Documented proxy_allow_duplicate_chunked. Message-ID: details: http://freenginx.org/hg/nginx-site/rev/851fed1c6352 branches: changeset: 3116:851fed1c6352 user: Maxim Dounin date: Sat Aug 23 02:35:32 2025 +0300 description: Documented proxy_allow_duplicate_chunked. diffstat: xml/en/docs/http/ngx_http_proxy_module.xml | 18 +++++++++++++++++- xml/ru/docs/http/ngx_http_proxy_module.xml | 18 +++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diffs (70 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="81">
@@ -39,6 +39,22 @@ location / {
+ +on | off +off +http +server +location +1.29.1 + + +Enables handling of responses with duplicate +
Transfer-Encoding: chunked
header fields. +By default, such responses are rejected since version 1.23.0. +
+ +
+ on | off off 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="81">
@@ -39,6 +39,22 @@ location / {
+ +on | off +off +http +server +location +1.29.1 + + +????????? ????????? ???????, ?????????? ????????????? +???? ?????????
Transfer-Encoding: chunked
. +?? ????????? ????? ?????? ??????????? ??????? ? ?????? 1.23.0. +
+ +
+ on | off off From mdounin at mdounin.ru Tue Aug 26 15:20:22 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 26 Aug 2025 18:20:22 +0300 Subject: [nginx] freenginx-1.29.1-RELEASE Message-ID: details: http://freenginx.org/hg/nginx/rev/8ccd477736c9 branches: changeset: 9419:8ccd477736c9 user: Maxim Dounin date: Tue Aug 26 18:04:09 2025 +0300 description: freenginx-1.29.1-RELEASE diffstat: docs/xml/nginx/changes.xml | 93 ++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 93 insertions(+), 0 deletions(-) diffs (103 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,99 @@
+ + + + +?????? ?????? ???????????? ???????? ?? HTTP/0.9 +?? ????????? ??????????? ??? ?????????; +????????? proxy_allow_http09 ????????? ?? ?????????. + + +now responses from proxied servers over HTTP/0.9 +are rejected as invalid by default; +the "proxy_allow_http09" directive allows processing of such responses. + + + + + +? ?????? "Host" ????????? ??????? +?????? ??????????? ????? ??????? ???????? ??????????. + + +stricter syntax checks are now applied +to the "Host" request header line. + + + + + +?????? ????????????? 1xx ?????? ???????????? ???????? ????????????. + + +now interim 1xx responses of proxied servers are ignored. + + + + + +??? ???????? SSL-???????????? ???????? +?????? ?????????????? ??????????? ?? IP-??????. + + +certificates for IP addresses are now supported +when verifying backend SSL certificates. + + + + + +????????? proxy_allow_duplicate_chunked.
+??????? ???????? ????????. +
+ +the "proxy_allow_duplicate_chunked" directive.
+Thanks to Gennady Bekasov. +
+
+ + + +??? ????????????? ????? ????? ? ?????? ??????? +?????? "_" ? ????????? ?????? ????????? ?????????????, +? ? IPv6-??????? ?? ??????????? ????????????? ??????????????? ???. + + +when using a host name in the request line, +the "_" and some other characters were not allowed, +as well as zone identifiers in IPv6 addresses. + + + + + +?? NetBSD ? ????? ????? ?????????? +????????? "shutdown() failed (22: Invalid argument)". + + +"shutdown() failed (22: Invalid argument)" alerts +might appear in logs on NetBSD. + + + + + +? ???????? ??????-???????. + + +in the mail proxy module. + + + +
+ + From mdounin at mdounin.ru Tue Aug 26 15:20:22 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 26 Aug 2025 18:20:22 +0300 Subject: [nginx] release-1.29.1 tag Message-ID: details: http://freenginx.org/hg/nginx/rev/352c8eb2b67c branches: changeset: 9420:352c8eb2b67c user: Maxim Dounin date: Tue Aug 26 18:04:10 2025 +0300 description: release-1.29.1 tag diffstat: .hgtags | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) diffs (8 lines): diff --git a/.hgtags b/.hgtags --- a/.hgtags +++ b/.hgtags @@ -486,3 +486,4 @@ 3e58802df709d487e366809eef77f6424433d187 e5a159b0c3821bf4e93b8e9ee34604238c93fd2a release-1.27.5 6731069e4b635d9dca49d6de04f0241cf3d856dd release-1.27.6 441d59c1052d602143764a5765d65c51388c1d7b release-1.29.0 +8ccd477736c998c0ea64c477b75c647cb28b356c release-1.29.1 From mdounin at mdounin.ru Tue Aug 26 15:20:48 2025 From: mdounin at mdounin.ru (=?iso-8859-1?q?Maxim_Dounin?=) Date: Tue, 26 Aug 2025 18:20:48 +0300 Subject: [nginx-site] freenginx-1.29.1 Message-ID: details: http://freenginx.org/hg/nginx-site/rev/be65dd520e81 branches: changeset: 3117:be65dd520e81 user: Maxim Dounin date: Tue Aug 26 18:17:55 2025 +0300 description: freenginx-1.29.1 diffstat: text/en/CHANGES | 27 +++++++++++++++++++++++++++ text/ru/CHANGES.ru | 28 ++++++++++++++++++++++++++++ xml/index.xml | 11 +++++++++++ xml/versions.xml | 1 + 4 files changed, 67 insertions(+), 0 deletions(-) diffs (103 lines): diff --git a/text/en/CHANGES b/text/en/CHANGES --- a/text/en/CHANGES +++ b/text/en/CHANGES @@ -1,4 +1,31 @@ +Changes with freenginx 1.29.1 26 Aug 2025 + + *) Change: now responses from proxied servers over HTTP/0.9 are rejected + as invalid by default; the "proxy_allow_http09" directive allows + processing of such responses. + + *) Change: stricter syntax checks are now applied to the "Host" request + header line. + + *) Feature: now interim 1xx responses of proxied servers are ignored. + + *) Feature: certificates for IP addresses are now supported when + verifying backend SSL certificates. + + *) Feature: the "proxy_allow_duplicate_chunked" directive. + Thanks to Gennady Bekasov. + + *) Bugfix: when using a host name in the request line, the "_" and some + other characters were not allowed, as well as zone identifiers in + IPv6 addresses. + + *) Workaround: "shutdown() failed (22: Invalid argument)" alerts might + appear in logs on NetBSD. + + *) Bugfix: in the mail proxy module. + + Changes with freenginx 1.29.0 08 Jul 2025 *) Change: the "directio" directive now works when returning responses 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,32 @@ +????????? ? freenginx 1.29.1 26.08.2025 + + *) ?????????: ?????? ?????? ???????????? ???????? ?? HTTP/0.9 ?? + ????????? ??????????? ??? ?????????; ????????? proxy_allow_http09 + ????????? ?? ?????????. + + *) ?????????: ? ?????? "Host" ????????? ??????? ?????? ??????????? ????? + ??????? ???????? ??????????. + + *) ??????????: ?????? ????????????? 1xx ?????? ???????????? ???????? + ????????????. + + *) ??????????: ??? ???????? SSL-???????????? ???????? ?????? + ?????????????? ??????????? ?? IP-??????. + + *) ??????????: ????????? proxy_allow_duplicate_chunked. + ??????? ???????? ????????. + + *) ???????????: ??? ????????????? ????? ????? ? ?????? ??????? ?????? + "_" ? ????????? ?????? ????????? ?????????????, ? ? IPv6-??????? ?? + ??????????? ????????????? ??????????????? ???. + + *) ?????????: ?? NetBSD ? ????? ????? ?????????? ????????? "shutdown() + failed (22: Invalid argument)". + + *) ???????????: ? ???????? ??????-???????. + + ????????? ? freenginx 1.29.0 08.07.2025 *) ?????????: ?????? ????????? directio ???????? ??? ???????, diff --git a/xml/index.xml b/xml/index.xml --- a/xml/index.xml +++ b/xml/index.xml @@ -8,6 +8,17 @@ + + +freenginx-1.29.1 +mainline version has been released, +featuring +improved handling of interim 1xx responses during proxying, +support for IP address certificates of proxied servers, +and more. + + + freenginx-1.29.0 diff --git a/xml/versions.xml b/xml/versions.xml --- a/xml/versions.xml +++ b/xml/versions.xml @@ -9,6 +9,7 @@ +