MINOR: uri_normalizer: Add a `strip-dot` normalizer

This normalizer removes "/./" segments from the path component.
Usually the dot refers to the current directory which renders those segments redundant.

See GitHub Issue #714.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index ce96c44..de91004 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -6013,6 +6013,7 @@
 
 http-request normalize-uri <normalizer> [ { if | unless } <condition> ]
 http-request normalize-uri path-merge-slashes [ { if | unless } <condition> ]
+http-request normalize-uri path-strip-dot [ { if | unless } <condition> ]
 http-request normalize-uri path-strip-dotdot [ full ] [ { if | unless } <condition> ]
 http-request normalize-uri percent-to-uppercase [ strict ] [ { if | unless } <condition> ]
 http-request normalize-uri query-sort-by-name [ { if | unless } <condition> ]
@@ -6035,6 +6036,14 @@
 
   The following normalizers are available:
 
+  - path-strip-dot: Removes "/./" segments within the "path" component.
+
+    Example:
+    - /.            -> /
+    - /./bar/       -> /bar/
+    - /a/./a        -> /a/a
+    - /.well-known/ -> /.well-known/ (no change)
+
   - path-strip-dotdot: Normalizes "/../" segments within the "path" component.
       This merges segments that attempt to access the parent directory with
       their preceding segment. Empty segments do not receive special treatment.
diff --git a/include/haproxy/action-t.h b/include/haproxy/action-t.h
index 43e6b1a..2e3edea 100644
--- a/include/haproxy/action-t.h
+++ b/include/haproxy/action-t.h
@@ -103,6 +103,7 @@
 
 enum act_normalize_uri {
 	ACT_NORMALIZE_URI_PATH_MERGE_SLASHES,
+	ACT_NORMALIZE_URI_PATH_STRIP_DOT,
 	ACT_NORMALIZE_URI_PATH_STRIP_DOTDOT,
 	ACT_NORMALIZE_URI_PATH_STRIP_DOTDOT_FULL,
 	ACT_NORMALIZE_URI_QUERY_SORT_BY_NAME,
diff --git a/include/haproxy/uri_normalizer.h b/include/haproxy/uri_normalizer.h
index 180936e..81c7e00 100644
--- a/include/haproxy/uri_normalizer.h
+++ b/include/haproxy/uri_normalizer.h
@@ -19,6 +19,7 @@
 #include <haproxy/uri_normalizer-t.h>
 
 enum uri_normalizer_err uri_normalizer_percent_upper(const struct ist input, int strict, struct ist *dst);
+enum uri_normalizer_err uri_normalizer_path_dot(const struct ist path, struct ist *dst);
 enum uri_normalizer_err uri_normalizer_path_dotdot(const struct ist path, int full, struct ist *dst);
 enum uri_normalizer_err uri_normalizer_path_merge_slashes(const struct ist path, struct ist *dst);
 enum uri_normalizer_err uri_normalizer_query_sort(const struct ist query, const char delim, struct ist *dst);
diff --git a/reg-tests/http-rules/normalize_uri.vtc b/reg-tests/http-rules/normalize_uri.vtc
index b997b31..9884b6c 100644
--- a/reg-tests/http-rules/normalize_uri.vtc
+++ b/reg-tests/http-rules/normalize_uri.vtc
@@ -8,7 +8,7 @@
 server s1 {
     rxreq
     txresp
-} -repeat 43 -start
+} -repeat 54 -start
 
 haproxy h1 -conf {
     defaults
@@ -82,6 +82,18 @@
 
         default_backend be
 
+    frontend fe_dot
+        bind "fd@${fe_dot}"
+
+        http-request set-var(txn.before) url
+        http-request normalize-uri path-strip-dot
+        http-request set-var(txn.after) url
+
+        http-response add-header before  %[var(txn.before)]
+        http-response add-header after  %[var(txn.after)]
+
+        default_backend be
+
     backend be
         server s1 ${s1_addr}:${s1_port}
 
@@ -312,3 +324,70 @@
     rxresp
     expect resp.status == 400
 } -run
+
+client c6 -connect ${h1_fe_dot_sock} {
+    txreq -url "/"
+    rxresp
+    expect resp.http.before == "/"
+    expect resp.http.after == "/"
+
+    txreq -url "/a/b"
+    rxresp
+    expect resp.http.before == "/a/b"
+    expect resp.http.after == "/a/b"
+
+    txreq -url "/."
+    rxresp
+    expect resp.http.before == "/."
+    expect resp.http.after == "/"
+
+    txreq -url "/./"
+    rxresp
+    expect resp.http.before == "/./"
+    expect resp.http.after == "/"
+
+    txreq -url "/a/."
+    rxresp
+    expect resp.http.before == "/a/."
+    expect resp.http.after == "/a/"
+
+    txreq -url "/a."
+    rxresp
+    expect resp.http.before == "/a."
+    expect resp.http.after == "/a."
+
+    txreq -url "/.a"
+    rxresp
+    expect resp.http.before == "/.a"
+    expect resp.http.after == "/.a"
+
+    txreq -url "/a/."
+    rxresp
+    expect resp.http.before == "/a/."
+    expect resp.http.after == "/a/"
+
+    txreq -url "/a/./"
+    rxresp
+    expect resp.http.before == "/a/./"
+    expect resp.http.after == "/a/"
+
+    txreq -url "/a/./a"
+    rxresp
+    expect resp.http.before == "/a/./a"
+    expect resp.http.after == "/a/a"
+
+    txreq -url "/a/../"
+    rxresp
+    expect resp.http.before == "/a/../"
+    expect resp.http.after == "/a/../"
+
+    txreq -url "/a/../a"
+    rxresp
+    expect resp.http.before == "/a/../a"
+    expect resp.http.after == "/a/../a"
+
+    txreq -url "/?a=/./"
+    rxresp
+    expect resp.http.before == "/?a=/./"
+    expect resp.http.after == "/?a=/./"
+} -run
diff --git a/src/http_act.c b/src/http_act.c
index 5da2d7d..df2bbe4 100644
--- a/src/http_act.c
+++ b/src/http_act.c
@@ -232,6 +232,23 @@
 
 			break;
 		}
+		case ACT_NORMALIZE_URI_PATH_STRIP_DOT: {
+			const struct ist path = http_get_path(uri);
+			struct ist newpath = ist2(replace->area, replace->size);
+
+			if (!isttest(path))
+				goto leave;
+
+			err = uri_normalizer_path_dot(iststop(path, '?'), &newpath);
+
+			if (err != URI_NORMALIZER_ERR_NONE)
+				break;
+
+			if (!http_replace_req_path(htx, newpath, 0))
+				goto fail_rewrite;
+
+			break;
+		}
 		case ACT_NORMALIZE_URI_PATH_STRIP_DOTDOT:
 		case ACT_NORMALIZE_URI_PATH_STRIP_DOTDOT_FULL: {
 			const struct ist path = http_get_path(uri);
@@ -350,6 +367,11 @@
 
 		rule->action = ACT_NORMALIZE_URI_PATH_MERGE_SLASHES;
 	}
+	else if (strcmp(args[cur_arg], "path-strip-dot") == 0) {
+		cur_arg++;
+
+		rule->action = ACT_NORMALIZE_URI_PATH_STRIP_DOT;
+	}
 	else if (strcmp(args[cur_arg], "path-strip-dotdot") == 0) {
 		cur_arg++;
 
diff --git a/src/uri_normalizer.c b/src/uri_normalizer.c
index ded9e1c..8d95936 100644
--- a/src/uri_normalizer.c
+++ b/src/uri_normalizer.c
@@ -75,6 +75,47 @@
 	return err;
 }
 
+/* Removes `/./` from the given path. */
+enum uri_normalizer_err uri_normalizer_path_dot(const struct ist path, struct ist *dst)
+{
+	enum uri_normalizer_err err;
+
+	const size_t size = istclear(dst);
+	struct ist newpath = *dst;
+
+	struct ist scanner = path;
+
+	/* The path will either be shortened or have the same length. */
+	if (size < istlen(path)) {
+		err = URI_NORMALIZER_ERR_ALLOC;
+		goto fail;
+	}
+
+	while (istlen(scanner) > 0) {
+		const struct ist segment = istsplit(&scanner, '/');
+
+		if (!isteq(segment, ist("."))) {
+			if (istcat(&newpath, segment, size) < 0) {
+				/* This is impossible, because we checked the size of the destination buffer. */
+				my_unreachable();
+				err = URI_NORMALIZER_ERR_INTERNAL_ERROR;
+				goto fail;
+			}
+
+			if (istend(segment) != istend(scanner))
+				newpath = __istappend(newpath, '/');
+		}
+	}
+
+	*dst = newpath;
+
+	return URI_NORMALIZER_ERR_NONE;
+
+  fail:
+
+	return err;
+}
+
 /* Merges `/../` with preceding path segments.
  *
  * If `full` is set to `0` then `/../` will be printed at the start of the resulting