MEDIUM: http-rules: Support an optional error message in http deny rules

It is now possible to set the error message to use when a deny rule is
executed. It may be a specific error file, adding "errorfile <file>" :

  http-request deny deny_status 400 errorfile /etc/haproxy/errorfiles/400badreq.http

It may also be an error file from an http-errors section, adding "errorfiles
<name>" :

  http-request deny errorfiles my-errors  # use 403 error from "my-errors" section

When defined, this error message is set in the HTTP transaction. The tarpit rule
is also concerned by this change.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index d12a3ae..93b9c65 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -4412,12 +4412,17 @@
   It takes one argument: "file name" It is the equivalent of the "del map"
   command from the stats socket, but can be triggered by an HTTP request.
 
-http-request deny [deny_status <status>] [ { if | unless } <condition> ]
+http-request deny [deny_status <status>] [ { errorfile | errorfiles } <err> ]
+                           [ { if | unless } <condition> ]
 
   This stops the evaluation of the rules and immediately rejects the request
   and emits an HTTP 403 error, or optionally the status code specified as an
   argument to "deny_status". The list of permitted status codes is limited to
-  those that can be overridden by the "errorfile" directive.
+  those that can be overridden by the "errorfile" directive. A specific error
+  message may be specified. It may be an error file, using the "errorfile"
+  keyword followed by the file containing the full HTTP response. It may also
+  be an error from an http-errors section, using the "errorfiles" keyword
+  followed by the section name.
   No further "http-request" rules are evaluated.
 
 http-request disable-l7-retry [ { if | unless } <condition> ]
@@ -4912,7 +4917,8 @@
   the frontend, the default mode is restored when HAProxy starts the backend
   rules evaluation.
 
-http-request tarpit [deny_status <status>] [ { if | unless } <condition> ]
+http-request tarpit [deny_status <status>] [ { errorfile | errorfiles } <err> ]
+                           [ { if | unless } <condition> ]
 
   This stops the evaluation of the rules and immediately blocks the request
   without responding for a delay specified by "timeout tarpit" or
@@ -4925,7 +4931,11 @@
   efficient against very dumb robots, and will significantly reduce the load
   on firewalls compared to a "deny" rule. But when facing "correctly"
   developed robots, it can make things worse by forcing haproxy and the front
-  firewall to support insane number of concurrent connections.
+  firewall to support insane number of concurrent connections. A specific error
+  message may be specified. It may be an error file, using the "errorfile"
+  keyword followed by the file containing the full HTTP response. It may also
+  be an error from an http-errors section, using the "errorfiles" keyword
+  followed by the section name.
   See also the "silent-drop" action.
 
 http-request track-sc0 <key> [table <table>] [ { if | unless } <condition> ]
@@ -5107,12 +5117,17 @@
   It takes one argument: "file name" It is the equivalent of the "del map"
   command from the stats socket, but can be triggered by an HTTP response.
 
-http-response deny [deny_status <status>] [ { if | unless } <condition> ]
+http-response deny [deny_status <status>] [ { errorfile | errorfiles } <err> ]
+                           [ { if | unless } <condition> ]
 
   This stops the evaluation of the rules and immediately rejects the response
   and emits an HTTP 502 error, or optionally the status code specified as an
   argument to "deny_status". The list of permitted status codes is limited to
-  those that can be overridden by the "errorfile" directive.
+  those that can be overridden by the "errorfile" directive. A specific error
+  message may be specified. It may be an error file, using the "errorfile"
+  keyword followed by the file containing the full HTTP response. It may also
+  be an error from an http-errors section, using the "errorfiles" keyword
+  followed by the section name.
   No further "http-response" rules are evaluated.
 
 http-response redirect <rule> [ { if | unless } <condition> ]
diff --git a/include/proto/http_htx.h b/include/proto/http_htx.h
index 0b25d41..4b980e2 100644
--- a/include/proto/http_htx.h
+++ b/include/proto/http_htx.h
@@ -29,6 +29,7 @@
 #include <types/http_htx.h>
 
 extern struct buffer http_err_chunks[HTTP_ERR_SIZE];
+extern struct list http_errors_list;
 
 struct htx_sl *http_get_stline(struct htx *htx);
 int http_find_header(const struct htx *htx, const struct ist name, struct http_hdr_ctx *ctx, int full);
diff --git a/include/types/action.h b/include/types/action.h
index 63dcfcf..6ec3f01 100644
--- a/include/types/action.h
+++ b/include/types/action.h
@@ -123,6 +123,10 @@
 			struct list fmt;       /* log-format compatible expression */
 			struct my_regex *re;   /* used by replace-header/value/uri/path */
 		} http;                        /* args used by some HTTP rules */
+		struct {
+			int status;            /* status code */
+			struct buffer *errmsg; /* HTTP error message, may be NULL */
+		} http_deny;                   /* args used by HTTP deny rules */
 		struct redirect_rule *redir;   /* redirect rule or "http-request redirect" */
 		struct {
 			char *ref;             /* MAP or ACL file name to update */
diff --git a/src/http_act.c b/src/http_act.c
index 218b1c4..774df14 100644
--- a/src/http_act.c
+++ b/src/http_act.c
@@ -785,52 +785,118 @@
 	return ACT_RET_PRS_OK;
 }
 
+/* Check an "http-request deny" action when an http-errors section is referenced.
+ *
+ * The function returns 1 in success case, otherwise, it returns 0 and err is
+ * filled.
+ */
+static int check_http_deny_action(struct act_rule *rule, struct proxy *px, char **err)
+{
+	struct http_errors *http_errs;
+	int status = (intptr_t)(rule->arg.act.p[0]);
+	int ret = 1;
+
+	list_for_each_entry(http_errs, &http_errors_list, list) {
+		if (strcmp(http_errs->id, (char *)rule->arg.act.p[1]) == 0) {
+			free(rule->arg.act.p[1]);
+			rule->arg.http_deny.status = status;
+			rule->arg.http_deny.errmsg = http_errs->errmsg[http_get_status_idx(status)];
+			if (!rule->arg.http_deny.errmsg)
+				ha_warning("Proxy '%s': status '%d' referenced by http deny rule "
+					   "not declared in http-errors section '%s'.\n",
+					   px->id, status, http_errs->id);
+			break;
+		}
+	}
+
+	if (&http_errs->list == &http_errors_list) {
+		memprintf(err, "unknown http-errors section '%s' referenced by http deny rule",
+			  (char *)rule->arg.act.p[1]);
+		free(rule->arg.act.p[1]);
+		ret = 0;
+	}
+
+	return ret;
+}
+
 /* Parse "deny" or "tarpit" actions for a request rule or "deny" action for a
- * response rule. It may take 2 optional arguments to define the status code. It
- * returns ACT_RET_PRS_OK on success, ACT_RET_PRS_ERR on error.
+ * response rule. It may take optional arguments to define the status code, the
+ * error file or the http-errors section to use. It returns ACT_RET_PRS_OK on
+ * success, ACT_RET_PRS_ERR on error.
  */
 static enum act_parse_ret parse_http_deny(const char **args, int *orig_arg, struct proxy *px,
 					  struct act_rule *rule, char **err)
 {
-	int code, hc, cur_arg;
+	int default_status, status, hc, cur_arg;
+
 
 	cur_arg = *orig_arg;
 	if (rule->from == ACT_F_HTTP_REQ) {
 		if (!strcmp(args[cur_arg-1], "tarpit")) {
 			rule->action = ACT_HTTP_REQ_TARPIT;
-			rule->arg.http.i = HTTP_ERR_500;
+			default_status = status = 500;
 		}
 		else {
 			rule->action = ACT_ACTION_DENY;
-			rule->arg.http.i = HTTP_ERR_403;
+			default_status = status = 403;
 		}
 	}
 	else {
-		rule->action = ACT_ACTION_DENY;;
-		rule->arg.http.i = HTTP_ERR_502;
+		rule->action = ACT_ACTION_DENY;
+		default_status = status = 502;
 	}
 	rule->flags |= ACT_FLAG_FINAL;
 
 	if (strcmp(args[cur_arg], "deny_status") == 0) {
 		cur_arg++;
 		if (!*args[cur_arg]) {
-			memprintf(err, "missing status code.\n");
+			memprintf(err, "'%s' expects <status_code> as argument", args[cur_arg-1]);
 			return ACT_RET_PRS_ERR;
 		}
 
-		code = atol(args[cur_arg]);
+		status = atol(args[cur_arg]);
 		cur_arg++;
 		for (hc = 0; hc < HTTP_ERR_SIZE; hc++) {
-			if (http_err_codes[hc] == code) {
-				rule->arg.http.i = hc;
+			if (http_err_codes[hc] == status)
 				break;
-			}
 		}
-		if (hc >= HTTP_ERR_SIZE)
-			memprintf(err, "status code %d not handled, using default code %d",
-				  code, http_err_codes[rule->arg.http.i]);
+		if (hc >= HTTP_ERR_SIZE) {
+			memprintf(err, "status code '%d' not handled, using default code '%d'",
+				  status, default_status);
+			status = default_status;
+			hc = http_get_status_idx(status);
+		}
 	}
 
+	if (strcmp(args[cur_arg], "errorfile") == 0) {
+		cur_arg++;
+		if (!*args[cur_arg]) {
+			memprintf(err, "'%s' expects <file> as argument", args[cur_arg-1]);
+			return ACT_RET_PRS_ERR;
+		}
+
+		rule->arg.http_deny.errmsg = http_load_errorfile(args[cur_arg], err);
+		if (!rule->arg.http_deny.errmsg)
+			return ACT_RET_PRS_ERR;
+		cur_arg++;
+	}
+	else if (strcmp(args[cur_arg], "errorfiles") == 0) {
+		cur_arg++;
+		if (!*args[cur_arg]) {
+			memprintf(err, "'%s' expects <http_errors_name> as argument", args[cur_arg-1]);
+			return ACT_RET_PRS_ERR;
+		}
+		/* Must be resolved during the config validity check */
+		rule->arg.act.p[0] = (void *)((intptr_t)status);
+		rule->arg.act.p[1] = strdup(args[cur_arg]);
+		rule->check_ptr = check_http_deny_action;
+		cur_arg++;
+		goto out;
+	}
+
+	rule->arg.http_deny.status = status;
+
+  out:
 	*orig_arg = cur_arg;
 	return ACT_RET_PRS_OK;
 }
diff --git a/src/http_ana.c b/src/http_ana.c
index 10ee241..82a9e14 100644
--- a/src/http_ana.c
+++ b/src/http_ana.c
@@ -2896,13 +2896,17 @@
 
 			case ACT_ACTION_DENY:
 				txn->flags |= TX_CLDENY;
-				txn->status = http_err_codes[rule->arg.http.i];
+				txn->status = rule->arg.http_deny.status;
+				if (rule->arg.http_deny.errmsg)
+					txn->errmsg = rule->arg.http_deny.errmsg;
 				rule_ret = HTTP_RULE_RES_DENY;
 				goto end;
 
 			case ACT_HTTP_REQ_TARPIT:
 				txn->flags |= TX_CLTARPIT;
-				txn->status = http_err_codes[rule->arg.http.i];
+				txn->status = rule->arg.http_deny.status;
+				if (rule->arg.http_deny.errmsg)
+					txn->errmsg = rule->arg.http_deny.errmsg;
 				rule_ret = HTTP_RULE_RES_DENY;
 				goto end;
 
@@ -3073,7 +3077,9 @@
 
 			case ACT_ACTION_DENY:
 				txn->flags |= TX_CLDENY;
-				txn->status = http_err_codes[rule->arg.http.i];
+				txn->status = rule->arg.http_deny.status;
+				if (rule->arg.http_deny.errmsg)
+					txn->errmsg = rule->arg.http_deny.errmsg;
 				rule_ret = HTTP_RULE_RES_DENY;
 				goto end;