]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: http-rules: add a new "ignore-empty" option to redirects.
authorWilly Tarreau <w@1wt.eu>
Thu, 2 Sep 2021 14:54:33 +0000 (16:54 +0200)
committerWilly Tarreau <w@1wt.eu>
Thu, 2 Sep 2021 15:06:18 +0000 (17:06 +0200)
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.

doc/configuration.txt
include/haproxy/http_ana-t.h
reg-tests/http-rules/map_redirect.map
reg-tests/http-rules/map_redirect.vtc
src/http_act.c
src/http_ana.c
src/http_rules.c

index f524dda3bf39592423b7301d60b74957dc63c7d9..987ea45c2354f72f472132ef4b601dfeca1a4136 100644 (file)
@@ -10015,6 +10015,13 @@ redirect scheme   <sch> [code <code>] <option> [{if | unless} <condition>]
         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 @@ redirect scheme   <sch> [code <code>] <option> [{if | unless} <condition>]
           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.
 
 
index 89d41dd7b3e4b7a31f9495aaba705c9a56da99fb..96845a45b26ee20db5aaea8e45943fc991329887 100644 (file)
@@ -105,6 +105,7 @@ enum {
        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 ) */
index a0cc02d59f30a789b669259e779cf7b599a97aa2..c4743f6d067529ad61a7e56818910841aab74ea4 100644 (file)
@@ -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
index 77e9b0dc3a082e2027c847b1d01daaebe8e6aebe..67b586b7aa6575ef4534755012c411a7bc14af90 100644 (file)
@@ -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 @@ haproxy h1 -conf {
   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 @@ client c1 -connect ${h1_fe1_sock} {
     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 @@ client c7 -connect ${h1_fe1_sock} {
 # 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$"
index c2fee04d644b02baeb5f896578956df30fb737c2..1e2bbdb5ffeae484785118ae8048a6f50df07622 100644 (file)
@@ -1767,7 +1767,6 @@ static enum act_parse_ret parse_http_redirect(const char **args, int *orig_arg,
        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 @@ static enum act_parse_ret parse_http_redirect(const char **args, int *orig_arg,
        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;
index b3605408850fec8e1edbfe300018609ab40bebd1..7a0417486d25ac3ecb64c5d3a1d933dcf436b708 100644 (file)
@@ -2338,8 +2338,9 @@ int http_response_forward_body(struct stream *s, struct channel *res, int an_bit
 }
 
 /* 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 @@ int http_apply_redirect_rule(struct redirect_rule *rule, struct stream *s, struc
                        }
                        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 @@ static enum rule_result http_req_get_intercept_rule(struct proxy *px, struct lis
                                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 @@ resume_execution:
                                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;
index 6ad1e9c2d0504b4d3442bfcdd27745b47d2d1f15..ce4a94554ed46d18db75648969ce8873ccfc814e 100644 (file)
@@ -379,6 +379,9 @@ struct redirect_rule *http_parse_redirect_rule(const char *file, int linenum, st
                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 @@ struct redirect_rule *http_parse_redirect_rule(const char *file, int linenum, st
                }
                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;
                }