MINOR: http: add a new "replace-path" action

This action is very similar to "replace-uri" except that it only acts on the
path component. This is assumed to better match users' expectations when they
used to rely on "replace-uri" in HTTP/1 because mostly origin forms were used
in H1 while mostly absolute URI form is used in H2, and their rules very often
start with a '/', and as such do not match.

It could help users to get this backported to 2.0 and 2.1.

(cherry picked from commit 262c3f1a00a901bfe4a3d6785155e1c02cf8039a)
Signed-off-by: Willy Tarreau <w@1wt.eu>
(cherry picked from commit a30e35ae9993696d5a2cc5b8db1d16702dd43fd4)
[wt: small context adjustments;  tested with both HTX and legacy]
Signed-off-by: Willy Tarreau <w@1wt.eu>
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 9307437..039742f 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -4541,6 +4541,30 @@
     # outputs:
     User-Agent: foo
 
+http-request replace-path <match-regex> <replace-fmt>
+                           [ { if | unless } <condition> ]
+
+  This works like "replace-header" except that it works on the request's path
+  component instead of a header. The path component starts at the first '/'
+  after an optional scheme+authority. It does contain the query string if any
+  is present. The replacement does not modify the scheme nor authority.
+
+  It is worth noting that regular expressions may be more expensive to evaluate
+  than certain ACLs, so rare replacements may benefit from a condition to avoid
+  performing the evaluation at all if it does not match.
+
+  Example:
+    # prefix /foo : turn /bar?q=1 into /foo/bar?q=1 :
+    http-request replace-path (.*) /foo\1
+
+    # suffix /foo : turn /bar?q=1 into /bar/foo?q=1 :
+    http-request replace-path ([^?]*)(\?(.*))? \1/foo\2
+
+    # strip /foo : turn /foo/bar?q=1 into /bar?q=1
+    http-request replace-path /foo/(.*) /\1
+    # or more efficient if only some requests match :
+    http-request replace-path /foo/(.*) /\1 if { url_beg /foo/ }
+
 http-request replace-uri <match-regex> <replace-fmt>
                            [ { if | unless } <condition> ]
 
@@ -4562,7 +4586,8 @@
   with HTTP/2, clients are encouraged to send absolute URIs only, which look
   like the ones HTTP/1 clients use to talk to proxies. Such partial replace-uri
   rules may then fail in HTTP/2 when they work in HTTP/1. Either the rules need
-  to be adapted to optionally match a scheme and authority.
+  to be adapted to optionally match a scheme and authority, or replace-path
+  should be used.
 
   Example:
     # rewrite all "http" absolute requests to "https":
diff --git a/src/cfgparse-listen.c b/src/cfgparse-listen.c
index f6bce0d..e7fe935 100644
--- a/src/cfgparse-listen.c
+++ b/src/cfgparse-listen.c
@@ -3909,7 +3909,7 @@
 	}
 	else if (!strcmp(args[0], "cliexp") || !strcmp(args[0], "reqrep")) {  /* replace request header from a regex */
 		if (!already_warned(WARN_REQREP_DEPRECATED))
-			ha_warning("parsing [%s:%d] : The '%s' directive is deprecated in favor of 'http-request replace-uri' and 'http-request replace-header' and will be removed in next version.\n", file, linenum, args[0]);
+			ha_warning("parsing [%s:%d] : The '%s' directive is deprecated in favor of 'http-request replace-uri', 'http-request replace-path', and 'http-request replace-header' and will be removed in next version.\n", file, linenum, args[0]);
 
 		if (*(args[2]) == 0) {
 			ha_alert("parsing [%s:%d] : '%s' expects <search> and <replace> as arguments.\n",
diff --git a/src/http_act.c b/src/http_act.c
index cf3d12c..14845aa 100644
--- a/src/http_act.c
+++ b/src/http_act.c
@@ -133,6 +133,8 @@
  * <rule>.arg.act.p[]. It builds a string in the trash from the format string
  * previously filled by function parse_replace_uri() and will execute the regex
  * in p[1] to replace the URI. It uses the format string present in act.p[2..3].
+ * The component to act on (path/uri) is taken from act.p[0] which contains 1
+ * for the path or 3 for the URI (values used by http_req_replace_stline()).
  * It always returns ACT_RET_CONT. If an error occurs, the action is canceled,
  * but the rule processing continues.
  */
@@ -154,6 +156,9 @@
 	else
 		uri = ist2(ci_head(&s->req) + s->txn->req.sl.rq.u, s->txn->req.sl.rq.u_l);
 
+	if (rule->arg.act.p[0] == (void *)1)
+		uri = http_get_path(uri); // replace path
+
 	if (!regex_exec_match2(rule->arg.act.p[1], uri.ptr, uri.len, MAX_MATCH, pmatch, 0))
 		goto leave;
 
@@ -166,8 +171,7 @@
 	if (len == -1)
 		goto leave;
 
-	/* 3 is the set-uri action */
-	http_replace_req_line(3, output->area, len, px, s);
+	http_replace_req_line((long)rule->arg.act.p[0], output->area, len, px, s);
 
 	ret = ACT_RET_CONT;
 
@@ -177,9 +181,9 @@
 	return ret;
 }
 
-/* parse a "replace-uri" http-request action.
+/* parse a "replace-uri" or "replace-path" http-request action.
  * This action takes 2 arguments (a regex and a replacement format string).
- * The resulting rule makes use of arg->act.p[0] to store the action (0 for now),
+ * The resulting rule makes use of arg->act.p[0] to store the action (1/3 for now),
  * p[1] to store the compiled regex, and arg->act.p[2..3] to store the log-format
  * list head. It returns ACT_RET_PRS_OK on success, ACT_RET_PRS_ERR on error.
  */
@@ -190,7 +194,11 @@
 	char *error = NULL;
 
 	rule->action = ACT_CUSTOM;
-	rule->arg.act.p[0] = (void *)0; // replace-uri
+	if (strcmp(args[cur_arg-1], "replace-path") == 0)
+		rule->arg.act.p[0] = (void *)1; // replace-path
+	else
+		rule->arg.act.p[0] = (void *)3; // replace-uri
+
 	rule->action_ptr = http_action_replace_uri;
 
 	if (!*args[cur_arg] || !*args[cur_arg+1] ||
@@ -699,6 +707,7 @@
 		{ "capture",    parse_http_req_capture },
 		{ "reject",     parse_http_action_reject },
 		{ "disable-l7-retry", parse_http_req_disable_l7_retry },
+		{ "replace-path", parse_replace_uri },
 		{ "replace-uri", parse_replace_uri },
 		{ "set-method", parse_set_req_line },
 		{ "set-path",   parse_set_req_line },