From 3e4bb23b21b77de6c951d292eb9ff0ada4f4125f Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Mon, 7 Sep 2026 04:01:34 +0200 Subject: [PATCH 1/2] Add return code redirects to Nginx URL rewriting pages This PR adds a "Redirect all requests" option to the server block and location URL Re-Writing pages in the Nginx Webserver module, backed by the Nginx return directive with a choice of 301, 302, 303, 307 or 308. The existing rewrite table only offers the redirect and permanent flags, which map to 302 and 301. The other codes requested on the forum are not supported by rewrite at all, so they can only be provided through return. The new option only manages return directives that are redirects. A bare URL form such as return https://example.com/; is shown as a 302, while non-redirect uses like return 404; are hidden from the page and never modified or removed. Example of the resulting config for a server block: return 308 "https://example.com$request_uri"; Fixes https://forum.virtualmin.com/t/137940 --- CHANGELOG.md | 1 + nginx/edit_lrewrite.cgi | 2 ++ nginx/edit_srewrite.cgi | 2 ++ nginx/lang/en | 10 ++++++ nginx/nginx-lib.pl | 67 +++++++++++++++++++++++++++++++++++++++++ nginx/save_lrewrite.cgi | 2 ++ nginx/save_srewrite.cgi | 2 ++ 7 files changed, 86 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff64086c9..710b8e2a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Add webserver logging controls to Usermin Configuration module * Add option to rotate Webmin and Usermin webserver logs using `logrotate` instead of periodically clearing them [#2821](https://github.com/webmin/webmin/pull/2821) * Add DNF 4 and 5 package hold management to the Software Package Updates module +* Add `return` based redirects with 301, 302, 303, 307 and 308 codes to the URL Re-Writing pages in the Nginx Webserver module [forum.virtualmin.com/t/137940](https://forum.virtualmin.com/t/137940) * Fix DNF update confirmations by previewing packages and dependencies that will be installed or updated * Fix PostgreSQL initialization on EL systems to use SCRAM-SHA-256 authentication by default * Fix IPsec host key generation with modern Libreswan [#2132](https://github.com/webmin/webmin/issues/2132) diff --git a/nginx/edit_lrewrite.cgi b/nginx/edit_lrewrite.cgi index 02af0ebda..fadd0528f 100755 --- a/nginx/edit_lrewrite.cgi +++ b/nginx/edit_lrewrite.cgi @@ -23,6 +23,8 @@ print &ui_table_start($text{'rewrite_header'}, undef, 2); print &nginx_rewrite_input("rewrite", $location); +print &nginx_return_input("return", $location); + print &nginx_onoff_input("rewrite_log", $location); print &ui_table_end(); diff --git a/nginx/edit_srewrite.cgi b/nginx/edit_srewrite.cgi index 3623859f4..b0c5b6b3b 100755 --- a/nginx/edit_srewrite.cgi +++ b/nginx/edit_srewrite.cgi @@ -19,6 +19,8 @@ print &ui_table_start($text{'rewrite_header'}, undef, 2); print &nginx_rewrite_input("rewrite", $server); +print &nginx_return_input("return", $server); + print &nginx_onoff_input("rewrite_log", $server); print &ui_table_end(); diff --git a/nginx/lang/en b/nginx/lang/en index d8f6f919a..f67b04d75 100644 --- a/nginx/lang/en +++ b/nginx/lang/en @@ -324,6 +324,16 @@ rewrite_redirect=Return 302 redirect rewrite_permanent=Return 301 redirect rewrite_efrom=Source path $1 contains spaces rewrite_eto=Destination URL $1 is missing or contains spaces +opt_return=Redirect all requests +rewrite_return_none=No redirect +rewrite_return_to=to URL +rewrite_return_301=301 (Moved permanently) +rewrite_return_302=302 (Found) +rewrite_return_303=303 (See other) +rewrite_return_307=307 (Temporary redirect) +rewrite_return_308=308 (Permanent redirect) +rewrite_ereturn=Redirect URL is missing or contains spaces +rewrite_ereturncode=Invalid redirect HTTP code location_create=Create Location location_edit=Edit Location diff --git a/nginx/nginx-lib.pl b/nginx/nginx-lib.pl index 77d4b27e6..42e7a545d 100644 --- a/nginx/nginx-lib.pl +++ b/nginx/nginx-lib.pl @@ -1462,6 +1462,73 @@ for(my $i=0; defined(my $from = $in->{$name."_from_".$i}); $i++) { &save_directive($parent, $name, \@obj); } +# list_return_redirect_codes() +# Returns the HTTP codes that the return directive accepts with a URL +sub list_return_redirect_codes +{ +return ( 301, 302, 303, 307, 308 ); +} + +# get_return_redirect(&object) +# Returns the code and URL of a return directive if it is a redirect, or an +# empty list for other uses such as return 404 +sub get_return_redirect +{ +my ($obj) = @_; +my @w = @{$obj->{'words'}}; +if (@w == 2 && &indexof($w[0], &list_return_redirect_codes()) >= 0) { + return @w; + } +elsif (@w == 1 && $w[0] =~ /^(https?:\/\/|\$scheme)/) { + # Single URL form defaults to a 302 redirect + return ( 302, $w[0] ); + } +return ( ); +} + +# nginx_return_input(name, &parent) +# Returns HTML for a redirect via the return directive +sub nginx_return_input +{ +my ($name, $parent) = @_; +return undef if (!&supported_directive($name, $parent)); +my $obj = &find($name, $parent); +my ($code, $url) = $obj ? &get_return_redirect($obj) : ( ); +# Leave a return directive that is not a redirect alone +return undef if ($obj && !$code); +my $sel = &ui_select($name."_code", $code || 301, + [ map { [ $_, $text{'rewrite_return_'.$_} ] } + &list_return_redirect_codes() ]); +return &ui_table_row($text{'opt_'.$name}, + &ui_opt_textbox($name, $url, 50, $text{'rewrite_return_none'}, + $sel." ".$text{'rewrite_return_to'}." ", 0, + [ $name."_code" ]), 3); +} + +# nginx_return_parse(name, &parent, &in) +# Updates the config with input from nginx_return_input +sub nginx_return_parse +{ +my ($name, $parent, $in) = @_; +return undef if (!&supported_directive($name, $parent)); +$in ||= \%in; +my $obj = &find($name, $parent); +# Never touch a return directive that was not shown in the form +return undef if ($obj && !&get_return_redirect($obj)); +if ($in->{$name."_def"}) { + &save_directive($parent, $name, [ ]); + } +else { + my $code = $in->{$name."_code"}; + &indexof($code, &list_return_redirect_codes()) >= 0 || + &error($text{'rewrite_ereturncode'}); + my $url = $in->{$name}; + $url =~ /^\S+$/ || &error($text{'rewrite_ereturn'}); + &save_directive($parent, $name, [ { 'name' => $name, + 'words' => [ $code, $url ] } ]); + } +} + # list_log_formats([&server]) # Returns a list of all log format names sub list_log_formats diff --git a/nginx/save_lrewrite.cgi b/nginx/save_lrewrite.cgi index 619a98f85..4bf090eb7 100755 --- a/nginx/save_lrewrite.cgi +++ b/nginx/save_lrewrite.cgi @@ -16,6 +16,8 @@ $location || &error($text{'location_egone'}); &nginx_rewrite_parse("rewrite", $location); +&nginx_return_parse("return", $location); + &nginx_onoff_parse("rewrite_log", $location); &flush_config_file_lines(); diff --git a/nginx/save_srewrite.cgi b/nginx/save_srewrite.cgi index f7ecb3e7f..a7b2368c4 100755 --- a/nginx/save_srewrite.cgi +++ b/nginx/save_srewrite.cgi @@ -14,6 +14,8 @@ $server || &error($text{'server_egone'}); &nginx_rewrite_parse("rewrite", $server); +&nginx_return_parse("return", $server); + &nginx_onoff_parse("rewrite_log", $server); &flush_config_file_lines(); From 41314af8785ae8ec4fd6c659287544d4d22ab4fa Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Mon, 7 Sep 2026 04:11:15 +0200 Subject: [PATCH 2/2] Update Nginx redirect target field to show an example placeholder --- nginx/lang/en | 3 ++- nginx/nginx-lib.pl | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nginx/lang/en b/nginx/lang/en index f67b04d75..c77639bab 100644 --- a/nginx/lang/en +++ b/nginx/lang/en @@ -326,7 +326,8 @@ rewrite_efrom=Source path $1 contains spaces rewrite_eto=Destination URL $1 is missing or contains spaces opt_return=Redirect all requests rewrite_return_none=No redirect -rewrite_return_to=to URL +rewrite_return_to=to +rewrite_return_eg=/uri or https://example.com$request_uri rewrite_return_301=301 (Moved permanently) rewrite_return_302=302 (Found) rewrite_return_303=303 (See other) diff --git a/nginx/nginx-lib.pl b/nginx/nginx-lib.pl index 42e7a545d..f69b079d1 100644 --- a/nginx/nginx-lib.pl +++ b/nginx/nginx-lib.pl @@ -1502,7 +1502,9 @@ my $sel = &ui_select($name."_code", $code || 301, return &ui_table_row($text{'opt_'.$name}, &ui_opt_textbox($name, $url, 50, $text{'rewrite_return_none'}, $sel." ".$text{'rewrite_return_to'}." ", 0, - [ $name."_code" ]), 3); + [ $name."_code" ], undef, + "placeholder=\"". + "e_escape($text{'rewrite_return_eg'})."\""), 3); } # nginx_return_parse(name, &parent, &in)