[MINOR] add the "force-persist" statement to force persistence on down servers

This is used to force access to down servers for some requests. This
is useful when validating that a change on a server correctly works
before enabling the server again.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index d626ade..8658fdc 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -1804,6 +1804,34 @@
   See also : "errorfile", "errorloc", "errorloc302"
 
 
+force-persist { if | unless } <condition>
+  Declare a condition to force persistence on down servers
+  May be used in sections:    defaults | frontend | listen | backend
+                                  no   |    yes   |   yes  |   yes
+
+  By default, requests are not dispatched to down servers. It is possible to
+  force this using "option persist", but it is unconditional and redispatches
+  to a valid server if "option redispatch" is set. That leaves with very little
+  possibilities to force some requests to reach a server which is artificially
+  marked down for maintenance operations.
+
+  The "force-persist" statement allows one to declare various ACL-based
+  conditions which, when met, will cause a request to ignore the down status of
+  a server and still try to connect to it. That makes it possible to start a
+  server, still replying an error to the health checks, and run a specially
+  configured browser to test the service. Among the handy methods, one could
+  use a specific source IP address, or a specific cookie. The cookie also has
+  the advantage that it can easily be added/removed on the browser from a test
+  page. Once the service is validated, it is then possible to open the service
+  to the world by returning a valid response to health checks.
+
+  The forced persistence is enabled when an "if" condition is met, or unless an
+  "unless" condition is met. The final redispatch is always disabled when this
+  is used.
+
+  See also : "option redispatch", "persist", and section 7 about ACL usage.
+
+
 fullconn <conns>
   Specify at what backend load the servers will reach their maxconn
   May be used in sections :   defaults | frontend | listen | backend
@@ -2935,7 +2963,7 @@
   If this option has been enabled in a "defaults" section, it can be disabled
   in a specific instance by prepending the "no" keyword before it.
 
-  See also : "option redispatch", "retries"
+  See also : "option redispatch", "retries", "force-persist"
 
 
 option redispatch
@@ -2962,7 +2990,7 @@
   If this option has been enabled in a "defaults" section, it can be disabled
   in a specific instance by prepending the "no" keyword before it.
 
-  See also : "redispatch", "retries"
+  See also : "redispatch", "retries", "force-persist"
 
 
 option smtpchk
diff --git a/include/types/proxy.h b/include/types/proxy.h
index a4c99cb..c4fb505 100644
--- a/include/types/proxy.h
+++ b/include/types/proxy.h
@@ -171,6 +171,7 @@
 	struct list block_cond;                 /* early blocking conditions (chained) */
 	struct list redirect_rules;             /* content redirecting rules (chained) */
 	struct list switching_rules;            /* content switching rules (chained) */
+	struct list force_persist_rules;        /* 'force-persist' rules (chained) */
 	struct list sticking_rules;             /* content sticking rules (chained) */
 	struct list storersp_rules;             /* content store response rules (chained) */
 	struct {                                /* TCP request processing */
@@ -297,6 +298,11 @@
 	} be;
 };
 
+struct force_persist_rule {
+	struct list list;			/* list linked to from the proxy */
+	struct acl_cond *cond;			/* acl condition to meet */
+};
+
 struct sticking_rule {
 	struct list list;                       /* list linked to from the proxy */
 	struct acl_cond *cond;                  /* acl condition to meet */
diff --git a/include/types/session.h b/include/types/session.h
index ecbb6c1..6c8cfa0 100644
--- a/include/types/session.h
+++ b/include/types/session.h
@@ -2,7 +2,7 @@
  * include/types/session.h
  * This file defines everything related to sessions.
  *
- * Copyright (C) 2000-2009 Willy Tarreau - w@1wt.eu
+ * Copyright (C) 2000-2010 Willy Tarreau - w@1wt.eu
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -47,7 +47,7 @@
 #define SN_ADDR_SET	0x00000004	/* this session's server address has been set */
 #define SN_BE_ASSIGNED	0x00000008	/* a backend was assigned. Conns are accounted. */
 
-/* unused:              0x00000010 */
+#define SN_FORCE_PRST	0x00000010	/* force persistence here, even if server is down */
 #define SN_MONITOR	0x00000020	/* this session comes from a monitoring system */
 #define SN_CURR_SESS	0x00000040	/* a connection is currently being counted on the server */
 #define SN_FRT_ADDR_SET	0x00000080	/* set if the frontend address has been filled */
diff --git a/src/backend.c b/src/backend.c
index 3d815ac..8b8b437 100644
--- a/src/backend.c
+++ b/src/backend.c
@@ -888,7 +888,8 @@
 		 * would bring us on the same server again. Note that t->srv is set in
 		 * this case.
 		 */
-		if ((t->flags & SN_DIRECT) && (t->be->options & PR_O_REDISP)) {
+		if (((t->flags & (SN_DIRECT|SN_FORCE_PRST)) == SN_DIRECT) &&
+		    (t->be->options & PR_O_REDISP)) {
 			t->flags &= ~(SN_DIRECT | SN_ASSIGNED | SN_ADDR_SET);
 			t->prev_srv = t->srv;
 			goto redispatch;
diff --git a/src/cfgparse.c b/src/cfgparse.c
index 9aae768..eea9d3e 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -849,6 +849,7 @@
 	LIST_INIT(&p->redirect_rules);
 	LIST_INIT(&p->mon_fail_cond);
 	LIST_INIT(&p->switching_rules);
+	LIST_INIT(&p->force_persist_rules);
 	LIST_INIT(&p->sticking_rules);
 	LIST_INIT(&p->storersp_rules);
 	LIST_INIT(&p->tcp_req.inspect_rules);
@@ -2038,6 +2039,58 @@
 		LIST_INIT(&rule->list);
 		LIST_ADDQ(&curproxy->switching_rules, &rule->list);
 	}
+	else if (!strcmp(args[0], "force-persist")) {
+		int pol = ACL_COND_NONE;
+		struct acl_cond *cond;
+		struct force_persist_rule *rule;
+
+		if (curproxy == &defproxy) {
+			Alert("parsing [%s:%d] : '%s' not allowed in 'defaults' section.\n", file, linenum, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+
+		if (warnifnotcap(curproxy, PR_CAP_FE|PR_CAP_BE, file, linenum, args[0], NULL))
+			err_code |= ERR_WARN;
+
+		if (!strcmp(args[1], "if"))
+			pol = ACL_COND_IF;
+		else if (!strcmp(args[1], "unless"))
+			pol = ACL_COND_UNLESS;
+
+		if (pol == ACL_COND_NONE) {
+			Alert("parsing [%s:%d] : '%s' requires either 'if' or 'unless' followed by a condition.\n",
+			      file, linenum, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+
+		if ((cond = parse_acl_cond((const char **)args + 2, &curproxy->acl, pol)) == NULL) {
+			Alert("parsing [%s:%d] : error detected while parsing a 'force-persist' rule.\n",
+			      file, linenum);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+
+		cond->file = file;
+		cond->line = linenum;
+		curproxy->acl_requires |= cond->requires;
+		if (cond->requires & ACL_USE_RTR_ANY) {
+			struct acl *acl;
+			const char *name;
+
+			acl = cond_find_require(cond, ACL_USE_RTR_ANY);
+			name = acl ? acl->name : "(unknown)";
+			Warning("parsing [%s:%d] : acl '%s' involves some response-only criteria which will be ignored.\n",
+				file, linenum, name);
+			err_code |= ERR_WARN;
+		}
+
+		rule = (struct force_persist_rule *)calloc(1, sizeof(*rule));
+		rule->cond = cond;
+		LIST_INIT(&rule->list);
+		LIST_ADDQ(&curproxy->force_persist_rules, &rule->list);
+	}
 	else if (!strcmp(args[0], "stick-table")) {
 		int myidx = 1;
 
diff --git a/src/checks.c b/src/checks.c
index 7eeced2..50e3190 100644
--- a/src/checks.c
+++ b/src/checks.c
@@ -310,7 +310,8 @@
 
 	FOREACH_ITEM_SAFE(pc, pc_bck, &s->pendconns, pc_end, struct pendconn *, list) {
 		struct session *sess = pc->sess;
-		if (sess->be->options & PR_O_REDISP) {
+		if ((sess->be->options & (PR_O_REDISP|PR_O_PERSIST)) == PR_O_REDISP &&
+		    !(sess->flags & SN_FORCE_PRST)) {
 			/* The REDISP option was specified. We will ignore
 			 * cookie and force to balance or use the dispatcher.
 			 */
diff --git a/src/proto_http.c b/src/proto_http.c
index 47b67e8..83e0518 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -3504,7 +3504,7 @@
 	s->req->cons->flags     = SI_FL_NONE;
 	s->req->flags &= ~(BF_SHUTW|BF_SHUTW_NOW|BF_AUTO_CONNECT|BF_WRITE_ERROR|BF_STREAMER|BF_STREAMER_FAST);
 	s->rep->flags &= ~(BF_SHUTR|BF_SHUTR_NOW|BF_READ_ATTACHED|BF_READ_ERROR|BF_READ_NOEXP|BF_STREAMER|BF_STREAMER_FAST|BF_WRITE_PARTIAL);
-	s->flags &= ~(SN_DIRECT|SN_ASSIGNED|SN_ADDR_SET|SN_BE_ASSIGNED);
+	s->flags &= ~(SN_DIRECT|SN_ASSIGNED|SN_ADDR_SET|SN_BE_ASSIGNED|SN_FORCE_PRST);
 	s->flags &= ~(SN_CURR_SESS|SN_REDIRECTABLE);
 	s->txn.meth = 0;
 	http_reset_txn(s);
@@ -5215,7 +5215,9 @@
 			struct server *srv = t->be->srv;
 			while (srv) {
 				if (strcmp(srv->id, asession->serverid) == 0) {
-					if (srv->state & SRV_RUNNING || t->be->options & PR_O_PERSIST) {
+					if ((srv->state & SRV_RUNNING) ||
+					    (t->be->options & PR_O_PERSIST) ||
+					    (t->flags & SN_FORCE_PRST)) {
 						/* we found the server and it's usable */
 						txn->flags &= ~TX_CK_MASK;
 						txn->flags |= TX_CK_VALID;
@@ -5411,7 +5413,9 @@
 					while (srv) {
 						if (srv->cookie && (srv->cklen == delim - p3) &&
 						    !memcmp(p3, srv->cookie, delim - p3)) {
-							if (srv->state & SRV_RUNNING || t->be->options & PR_O_PERSIST) {
+							if ((srv->state & SRV_RUNNING) ||
+							    (t->be->options & PR_O_PERSIST) ||
+							    (t->flags & SN_FORCE_PRST)) {
 								/* we found the server and it's usable */
 								txn->flags &= ~TX_CK_MASK;
 								txn->flags |= TX_CK_VALID;
diff --git a/src/session.c b/src/session.c
index 20a5886..36fe50d 100644
--- a/src/session.c
+++ b/src/session.c
@@ -276,7 +276,8 @@
 	 * bit to ignore any persistence cookie. We won't count a retry nor a
 	 * redispatch yet, because this will depend on what server is selected.
 	 */
-	if (s->srv && s->conn_retries == 0 && s->be->options & PR_O_REDISP) {
+	if (s->srv && s->conn_retries == 0 &&
+	    s->be->options & PR_O_REDISP && !(s->flags & SN_FORCE_PRST)) {
 		if (may_dequeue_tasks(s->srv, s->be))
 			process_srv_queue(s->srv);
 
@@ -543,12 +544,15 @@
 }
 
 /* This stream analyser checks the switching rules and changes the backend
- * if appropriate. The default_backend rule is also considered.
+ * if appropriate. The default_backend rule is also considered, then the
+ * target backend's forced persistence rules are also evaluated last if any.
  * It returns 1 if the processing can continue on next analysers, or zero if it
  * either needs more data or wants to immediately abort the request.
  */
 int process_switching_rules(struct session *s, struct buffer *req, int an_bit)
 {
+	struct force_persist_rule *prst_rule;
+
 	req->analysers &= ~an_bit;
 	req->analyse_exp = TICK_ETERNITY;
 
@@ -594,6 +598,26 @@
 	if (s->fe == s->be)
 		s->req->analysers &= ~AN_REQ_HTTP_PROCESS_BE;
 
+	/* as soon as we know the backend, we must check if we have a matching forced
+	 * persistence rule, and report that in the session.
+	 */
+	list_for_each_entry(prst_rule, &s->be->force_persist_rules, list) {
+		int ret = 1;
+
+		if (prst_rule->cond) {
+	                ret = acl_exec_cond(prst_rule->cond, s->be, s, &s->txn, ACL_DIR_REQ);
+			ret = acl_pass(ret);
+			if (prst_rule->cond->pol == ACL_COND_UNLESS)
+				ret = !ret;
+		}
+
+		if (ret) {
+			/* no rule, or the rule matches */
+			s->flags |= SN_FORCE_PRST;
+			break;
+		}
+	}
+
 	return 1;
 
  sw_failed:
@@ -669,7 +693,9 @@
 							struct server *srv;
 
 							srv = container_of(node, struct server, conf.id);
-							if ((srv->state & SRV_RUNNING) || (px->options & PR_O_PERSIST)) {
+							if ((srv->state & SRV_RUNNING) ||
+							    (px->options & PR_O_PERSIST) ||
+							    (s->flags & SN_FORCE_PRST)) {
 								s->flags |= SN_DIRECT | SN_ASSIGNED;
 								s->srv = srv;
 							}