MINOR: http-rules: add a new "ignore-empty" option to redirects.

Sometimes it is convenient to remap large sets of URIs to new ones (e.g.
after a site migration for example). This can be achieved using
"http-request redirect" combined with maps, but one difficulty there is
that non-matching entries will return an empty response. In order to
avoid this, duplicating the operation as an ACL condition ending in
"-m found" is possible but it becomes complex and error-prone while it's
known that an empty URL is not valid in a location header.

This patch addresses this by improving the redirect rules to be able to
simply ignore the rule and skip to the next one if the result of the
evaluation of the "location" expression is empty. However in order not
to break existing setups, it requires a new "ignore-empty" keyword.

There used to be an ACT_FLAG_FINAL on redirect rules that's used during
the parsing to emit a warning if followed by another rule, so here we
only set it if the option is not there. The http_apply_redirect_rule()
function now returns a 3rd value to mention that it did nothing and
that this was not an error, so that callers can just ignore the rule.
The regular "redirect" rules were not modified however since this does
not apply there.

The map_redirect VTC was completed with such a test and updated to 2.5
and an example was added into the documentation.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index f524dda..987ea45 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -10015,6 +10015,13 @@
         It can be useful to ensure that search engines will only see one URL.
         For this, a return code 301 is preferred.
 
+      - "ignore-empty"
+        This keyword only has effect when a location is produced using a log
+        format expression (i.e. when used in http-request or http-response).
+        It indicates that if the result of the expression is empty, the rule
+        should silently be skipped. The main use is to allow mass-redirects
+        of known paths using a simple map.
+
       - "set-cookie NAME[=value]"
         A "Set-Cookie" header will be added with NAME (and optionally "=value")
         to the response. This is sometimes used to indicate that a user has
@@ -10057,6 +10064,10 @@
           http://www.%[hdr(host)]%[capture.req.uri]  \
           unless { hdr_beg(host) -i www }
 
+  Example: permanently redirect only old URLs to new ones
+        http-request redirect code 301 location               \
+          %[path,map_str(old-blog-articles.map)] ignore-empty
+
   See section 7 about ACL usage.
 
 
diff --git a/include/haproxy/http_ana-t.h b/include/haproxy/http_ana-t.h
index 89d41dd..96845a4 100644
--- a/include/haproxy/http_ana-t.h
+++ b/include/haproxy/http_ana-t.h
@@ -105,6 +105,7 @@
 	REDIRECT_FLAG_DROP_QS = 1,	/* drop query string */
 	REDIRECT_FLAG_APPEND_SLASH = 2,	/* append a slash if missing at the end */
 	REDIRECT_FLAG_FROM_REQ = 4,     /* redirect rule on the request path */
+	REDIRECT_FLAG_IGNORE_EMPTY = 8, /* silently ignore empty location expressions */
 };
 
 /* Redirect types (location, prefix, extended ) */
diff --git a/reg-tests/http-rules/map_redirect.map b/reg-tests/http-rules/map_redirect.map
index a0cc02d..c4743f6 100644
--- a/reg-tests/http-rules/map_redirect.map
+++ b/reg-tests/http-rules/map_redirect.map
@@ -1,3 +1,5 @@
 # These entries are used for http-request redirect rules
 example.org https://www.example.org
 subdomain.example.org https://www.subdomain.example.org
+
+/path/to/old/file  /path/to/new/file
diff --git a/reg-tests/http-rules/map_redirect.vtc b/reg-tests/http-rules/map_redirect.vtc
index 77e9b0d..67b586b 100644
--- a/reg-tests/http-rules/map_redirect.vtc
+++ b/reg-tests/http-rules/map_redirect.vtc
@@ -1,5 +1,5 @@
 varnishtest "haproxy host header: map / redirect tests"
-#REQUIRE_OPTIONS=PCRE|PCRE2
+feature cmd "$HAPROXY_PROGRAM -cc 'version_atleast(2.5-dev5) && (feature(PCRE) || feature(PCRE2))'"
 feature ignore_unknown_macro
 
 
@@ -43,6 +43,9 @@
   frontend fe1
     bind "fd@${fe1}"
 
+    # automatically redirect matching paths from maps but skip rule on no-match
+    http-request redirect code 301 location %[path,map_str(${testdir}/map_redirect.map)] ignore-empty
+
     # redirect Host: example.org / subdomain.example.org
     http-request redirect prefix %[req.hdr(Host),lower,regsub(:\d+$,,),map_str(${testdir}/map_redirect.map)] code 301 if { hdr(Host),lower,regsub(:\d+$,,),map_str(${testdir}/map_redirect.map) -m found }
 
@@ -83,6 +86,12 @@
     rxresp
     expect resp.status == 301
     expect resp.http.location ~ "https://www.example.org"
+
+    txreq -url /path/to/old/file
+    rxresp
+    expect resp.status == 301
+    expect resp.http.location ~ "/path/to/new/file"
+
     # Closes connection
 } -run
 
@@ -145,7 +154,7 @@
 # cli show maps
 haproxy h1 -cli {
     send "show map ${testdir}/map_redirect.map"
-    expect ~ "^0x[a-f0-9]+ example\\.org https://www\\.example\\.org\\n0x[a-f0-9]+ subdomain\\.example\\.org https://www\\.subdomain\\.example\\.org\\n$"
+    expect ~ "^0x[a-f0-9]+ example\\.org https://www\\.example\\.org\\n0x[a-f0-9]+ subdomain\\.example\\.org https://www\\.subdomain\\.example\\.org\\n0x[a-f0-9]+ /path/to/old/file /path/to/new/file\n$"
 
     send "show map ${testdir}/map_redirect-be.map"
     expect ~ "^0x[a-f0-9]+ test1\\.example\\.com test1_be\\n0x[a-f0-9]+ test1\\.example\\.invalid test1_be\\n0x[a-f0-9]+ test2\\.example\\.com test2_be\\n$"
diff --git a/src/http_act.c b/src/http_act.c
index c2fee04..1e2bbdb 100644
--- a/src/http_act.c
+++ b/src/http_act.c
@@ -1767,7 +1767,6 @@
 	int dir, cur_arg;
 
 	rule->action = ACT_HTTP_REDIR;
-	rule->flags |= ACT_FLAG_FINAL;
 	rule->release_ptr = release_http_redir;
 
 	cur_arg = *orig_arg;
@@ -1776,6 +1775,9 @@
 	if ((redir = http_parse_redirect_rule(px->conf.args.file, px->conf.args.line, px, &args[cur_arg], err, 1, dir)) == NULL)
 		return ACT_RET_PRS_ERR;
 
+	if (!(redir->flags & REDIRECT_FLAG_IGNORE_EMPTY))
+		rule->flags |= ACT_FLAG_FINAL;
+
 	rule->arg.redir = redir;
 	rule->cond = redir->cond;
 	redir->cond = NULL;
diff --git a/src/http_ana.c b/src/http_ana.c
index b360540..7a04174 100644
--- a/src/http_ana.c
+++ b/src/http_ana.c
@@ -2338,8 +2338,9 @@
 }
 
 /* Perform an HTTP redirect based on the information in <rule>. The function
- * returns zero on success, or zero in case of a, irrecoverable error such
- * as too large a request to build a valid response.
+ * returns zero in case of an irrecoverable error such as too large a request
+ * to build a valid response, 1 in case of successful redirect (hence the rule
+ * is final), or 2 if the rule has to be silently skipped.
  */
 int http_apply_redirect_rule(struct redirect_rule *rule, struct stream *s, struct http_txn *txn)
 {
@@ -2482,9 +2483,13 @@
 			}
 			else {
 				/* add location with executing log format */
-				chunk->data += build_logline(s, chunk->area + chunk->data,
-							     chunk->size - chunk->data,
-							     &rule->rdr_fmt);
+				int len = build_logline(s, chunk->area + chunk->data,
+				                        chunk->size - chunk->data,
+				                        &rule->rdr_fmt);
+				if (!len && rule->flags & REDIRECT_FLAG_IGNORE_EMPTY)
+					return 2;
+
+				chunk->data += len;
 			}
 			break;
 	}
@@ -2791,11 +2796,15 @@
 				rule_ret = HTTP_RULE_RES_DENY;
 				goto end;
 
-			case ACT_HTTP_REDIR:
-				rule_ret = HTTP_RULE_RES_ABRT;
-				if (!http_apply_redirect_rule(rule->arg.redir, s, txn))
-					rule_ret = HTTP_RULE_RES_ERROR;
+			case ACT_HTTP_REDIR: {
+				int ret = http_apply_redirect_rule(rule->arg.redir, s, txn);
+
+				if (ret == 2) // 2 == skip
+					break;
+
+				rule_ret = ret ? HTTP_RULE_RES_ABRT : HTTP_RULE_RES_ERROR;
 				goto end;
+			}
 
 			/* other flags exists, but normally, they never be matched. */
 			default:
@@ -2916,12 +2925,15 @@
 				rule_ret = HTTP_RULE_RES_DENY;
 				goto end;
 
-			case ACT_HTTP_REDIR:
-				rule_ret = HTTP_RULE_RES_ABRT;
-				if (!http_apply_redirect_rule(rule->arg.redir, s, txn))
-					rule_ret = HTTP_RULE_RES_ERROR;
-				goto end;
+			case ACT_HTTP_REDIR: {
+				int ret = http_apply_redirect_rule(rule->arg.redir, s, txn);
 
+				if (ret == 2) // 2 == skip
+					break;
+
+				rule_ret = ret ? HTTP_RULE_RES_ABRT : HTTP_RULE_RES_ERROR;
+				goto end;
+			}
 			/* other flags exists, but normally, they never be matched. */
 			default:
 				break;
diff --git a/src/http_rules.c b/src/http_rules.c
index 6ad1e9c..ce4a945 100644
--- a/src/http_rules.c
+++ b/src/http_rules.c
@@ -379,6 +379,9 @@
 		else if (strcmp(args[cur_arg], "append-slash") == 0) {
 			flags |= REDIRECT_FLAG_APPEND_SLASH;
 		}
+		else if (strcmp(args[cur_arg], "ignore-empty") == 0) {
+			flags |= REDIRECT_FLAG_IGNORE_EMPTY;
+		}
 		else if (strcmp(args[cur_arg], "if") == 0 ||
 			 strcmp(args[cur_arg], "unless") == 0) {
 			cond = build_acl_cond(file, linenum, &curproxy->acl, curproxy, (const char **)args + cur_arg, errmsg);
@@ -390,7 +393,7 @@
 		}
 		else {
 			memprintf(errmsg,
-			          "expects 'code', 'prefix', 'location', 'scheme', 'set-cookie', 'clear-cookie', 'drop-query' or 'append-slash' (was '%s')",
+			          "expects 'code', 'prefix', 'location', 'scheme', 'set-cookie', 'clear-cookie', 'drop-query', 'ignore-empty' or 'append-slash' (was '%s')",
 			          args[cur_arg]);
 			return NULL;
 		}