MEDIUM: session: implement the "use-server" directive

Sometimes it is desirable to forward a particular request to a specific
server without having to declare a dedicated backend for this server. This
can be achieved using the "use-server" rules. These rules are evaluated after
the "redirect" rules and before evaluating cookies, and they have precedence
on them. There may be as many "use-server" rules as desired. All of these
rules are evaluated in their declaration order, and the first one which
matches will assign the server.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 2ede208..337cfad 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -1118,6 +1118,7 @@
 timeout tarpit                            X          X         X         X
 transparent                 (deprecated)  X          -         X         X
 use_backend                               -          X         X         -
+use-server                                -          -         X         X
 ------------------------------------+----------+----------+---------+---------
  keyword                              defaults   frontend   listen    backend
 
@@ -6597,6 +6598,60 @@
   See also: "default_backend", "tcp-request", and section 7 about ACLs.
 
 
+use-server <server> if <condition>
+use-server <server> unless <condition>
+  Only use a specific server if/unless an ACL-based condition is matched.
+  May be used in sections :   defaults | frontend | listen | backend
+                                  no   |    no    |   yes  |   yes
+  Arguments :
+    <server>   is the name of a valid server in the same backend section.
+
+    <condition> is a condition composed of ACLs, as described in section 7.
+
+  By default, connections which arrive to a backend are load-balanced across
+  the available servers according to the configured algorithm, unless a
+  persistence mechanism such as a cookie is used and found in the request.
+
+  Sometimes it is desirable to forward a particular request to a specific
+  server without having to declare a dedicated backend for this server. This
+  can be achieved using the "use-server" rules. These rules are evaluated after
+  the "redirect" rules and before evaluating cookies, and they have precedence
+  on them. There may be as many "use-server" rules as desired. All of these
+  rules are evaluated in their declaration order, and the first one which
+  matches will assign the server.
+
+  If a rule designates a server which is down, and "option persist" is not used
+  and no force-persist rule was validated, it is ignored and evaluation goes on
+  with the next rules until one matches.
+
+  In the first form, the server will be used if the condition is met. In the
+  second form, the server will be used if the condition is not met. If no
+  condition is valid, the processing continues and the server will be assigned
+  according to other persistence mechanisms.
+
+  Note that even if a rule is matched, cookie processing is still performed but
+  does not assign the server. This allows prefixed cookies to have their prefix
+  stripped.
+
+  The "use-server" statement works both in HTTP and TCP mode. This makes it
+  suitable for use with content-based inspection. For instance, a server could
+  be selected in a farm according to the TLS SNI field. And if these servers
+  have their weight set to zero, they will not be used for other traffic.
+
+  Example :
+     # intercept incoming TLS requests based on the SNI field
+     use-server www if { req_ssl_sni -i www.example.com }
+     server     www 192.168.0.1:443 weight 0
+     use-server mail if { req_ssl_sni -i mail.example.com }
+     server     mail 192.168.0.1:587 weight 0
+     use-server imap if { req_ssl_sni -i imap.example.com }
+     server     mail 192.168.0.1:993 weight 0
+     # all the rest is forwarded to this server
+     server  default 192.168.0.2:443 check
+
+  See also: "use_backend", serction 5 about server and section 7 about ACLs.
+
+
 5. Server and default-server options
 ------------------------------------
 
diff --git a/include/types/buffers.h b/include/types/buffers.h
index 42e2a56..6280c4f 100644
--- a/include/types/buffers.h
+++ b/include/types/buffers.h
@@ -144,12 +144,13 @@
 #define AN_REQ_SWITCHING_RULES  0x00000010  /* apply the switching rules */
 #define AN_REQ_INSPECT_BE       0x00000020  /* inspect request contents in the backend */
 #define AN_REQ_HTTP_PROCESS_BE  0x00000040  /* process the backend's HTTP part */
-#define AN_REQ_HTTP_INNER       0x00000080  /* inner processing of HTTP request */
-#define AN_REQ_HTTP_TARPIT      0x00000100  /* wait for end of HTTP tarpit */
-#define AN_REQ_HTTP_BODY        0x00000200  /* inspect HTTP request body */
-#define AN_REQ_STICKING_RULES   0x00000400  /* table persistence matching */
-#define AN_REQ_PRST_RDP_COOKIE  0x00000800  /* persistence on rdp cookie */
-#define AN_REQ_HTTP_XFER_BODY   0x00001000  /* forward request body */
+#define AN_REQ_SRV_RULES        0x00000080  /* use-server rules */
+#define AN_REQ_HTTP_INNER       0x00000100  /* inner processing of HTTP request */
+#define AN_REQ_HTTP_TARPIT      0x00000200  /* wait for end of HTTP tarpit */
+#define AN_REQ_HTTP_BODY        0x00000400  /* inspect HTTP request body */
+#define AN_REQ_STICKING_RULES   0x00000800  /* table persistence matching */
+#define AN_REQ_PRST_RDP_COOKIE  0x00001000  /* persistence on rdp cookie */
+#define AN_REQ_HTTP_XFER_BODY   0x00002000  /* forward request body */
 
 /* response analysers */
 #define AN_RES_INSPECT          0x00010000  /* content inspection */
diff --git a/include/types/proxy.h b/include/types/proxy.h
index 26cdfe4..d69a914 100644
--- a/include/types/proxy.h
+++ b/include/types/proxy.h
@@ -205,6 +205,7 @@
 	struct list persist_rules;		/* 'force-persist' and 'ignore-persist' rules (chained) */
 	struct list sticking_rules;             /* content sticking rules (chained) */
 	struct list storersp_rules;             /* content store response rules (chained) */
+	struct list server_rules;               /* server switching rules (chained) */
 	struct {                                /* TCP request processing */
 		unsigned int inspect_delay;     /* inspection delay */
 		struct list inspect_rules;      /* inspection rules */
@@ -350,6 +351,15 @@
 	} be;
 };
 
+struct server_rule {
+	struct list list;			/* list linked to from the proxy */
+	struct acl_cond *cond;			/* acl condition to meet */
+	union {
+		struct server *ptr;		/* target server */
+		char *name;			/* target server name during config parsing */
+	} srv;
+};
+
 struct persist_rule {
 	struct list list;			/* list linked to from the proxy */
 	struct acl_cond *cond;			/* acl condition to meet */
diff --git a/src/cfgparse.c b/src/cfgparse.c
index 5996e26..d827d4c 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -2702,6 +2702,47 @@
 		LIST_INIT(&rule->list);
 		LIST_ADDQ(&curproxy->switching_rules, &rule->list);
 	}
+	else if (strcmp(args[0], "use-server") == 0) {
+		struct server_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_BE, file, linenum, args[0], NULL))
+			err_code |= ERR_WARN;
+
+		if (*(args[1]) == 0) {
+			Alert("parsing [%s:%d] : '%s' expects a server name.\n", file, linenum, args[0]);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+
+		if (strcmp(args[2], "if") != 0 && strcmp(args[2], "unless") != 0) {
+			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 = build_acl_cond(file, linenum, curproxy, (const char **)args + 2)) == NULL) {
+			Alert("parsing [%s:%d] : error detected while parsing switching rule.\n",
+			      file, linenum);
+			err_code |= ERR_ALERT | ERR_FATAL;
+			goto out;
+		}
+
+		err_code |= warnif_cond_requires_resp(cond, file, linenum);
+
+		rule = (struct server_rule *)calloc(1, sizeof(*rule));
+		rule->cond = cond;
+		rule->srv.name = strdup(args[1]);
+		LIST_INIT(&rule->list);
+		LIST_ADDQ(&curproxy->server_rules, &rule->list);
+		curproxy->be_req_ana |= AN_REQ_SRV_RULES;
+	}
 	else if ((!strcmp(args[0], "force-persist")) ||
 		 (!strcmp(args[0], "ignore-persist"))) {
 		struct persist_rule *rule;
@@ -5603,6 +5644,7 @@
 
 	while (curproxy != NULL) {
 		struct switching_rule *rule;
+		struct server_rule *srule;
 		struct sticking_rule *mrule;
 		struct tcp_rule *trule;
 		struct listener *listener;
@@ -5792,6 +5834,20 @@
 				target->bind_proc = curproxy->bind_proc ?
 					(target->bind_proc | curproxy->bind_proc) : 0;
 			}
+		}
+
+		/* find the target proxy for 'use_backend' rules */
+		list_for_each_entry(srule, &curproxy->server_rules, list) {
+			struct server *target = findserver(curproxy, srule->srv.name);
+
+			if (!target) {
+				Alert("config : %s '%s' : unable to find server '%s' referenced in a 'use-server' rule.\n",
+				      proxy_type_str(curproxy), curproxy->id, srule->srv.name);
+				cfgerr++;
+				continue;
+			}
+			free((void *)srule->srv.name);
+			srule->srv.ptr = target;
 		}
 
 		/* find the target table for 'stick' rules */
diff --git a/src/haproxy.c b/src/haproxy.c
index 2858517..c0b7d51 100644
--- a/src/haproxy.c
+++ b/src/haproxy.c
@@ -797,6 +797,7 @@
 	struct hdr_exp *exp, *expb;
 	struct acl *acl, *aclb;
 	struct switching_rule *rule, *ruleb;
+	struct server_rule *srule, *sruleb;
 	struct redirect_rule *rdr, *rdrb;
 	struct wordlist *wl, *wlb;
 	struct cond_wordlist *cwl, *cwlb;
@@ -891,6 +892,13 @@
 			free(acl);
 		}
 
+		list_for_each_entry_safe(srule, sruleb, &p->server_rules, list) {
+			LIST_DEL(&srule->list);
+			prune_acl_cond(srule->cond);
+			free(srule->cond);
+			free(srule);
+		}
+
 		list_for_each_entry_safe(rule, ruleb, &p->switching_rules, list) {
 			LIST_DEL(&rule->list);
 			prune_acl_cond(rule->cond);
diff --git a/src/proto_http.c b/src/proto_http.c
index 3f6ec04..9af3f69 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -6221,7 +6221,7 @@
 				 * empty cookies and mark them as invalid.
 				 * The same behaviour is applied when persistence must be ignored.
 				 */
-				if ((delim == val_beg) || (t->flags & SN_IGNORE_PRST))
+				if ((delim == val_beg) || (t->flags & (SN_IGNORE_PRST | SN_ASSIGNED)))
 					srv = NULL;
 
 				while (srv) {
diff --git a/src/proxy.c b/src/proxy.c
index 84d430a..7f15bb2 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -427,6 +427,7 @@
 	LIST_INIT(&p->redirect_rules);
 	LIST_INIT(&p->mon_fail_cond);
 	LIST_INIT(&p->switching_rules);
+	LIST_INIT(&p->server_rules);
 	LIST_INIT(&p->persist_rules);
 	LIST_INIT(&p->sticking_rules);
 	LIST_INIT(&p->storersp_rules);
diff --git a/src/session.c b/src/session.c
index e5b76eb..b90b254 100644
--- a/src/session.c
+++ b/src/session.c
@@ -1041,6 +1041,55 @@
 	return 0;
 }
 
+/* This stream analyser works on a request. It applies all use-server rules on
+ * it then returns 1. The data must already be present in the buffer otherwise
+ * they won't match. It always returns 1.
+ */
+static int process_server_rules(struct session *s, struct buffer *req, int an_bit)
+{
+	struct proxy *px = s->be;
+	struct server_rule *rule;
+
+	DPRINTF(stderr,"[%u] %s: session=%p b=%p, exp(r,w)=%u,%u bf=%08x bl=%d analysers=%02x\n",
+		now_ms, __FUNCTION__,
+		s,
+		req,
+		req->rex, req->wex,
+		req->flags,
+		req->l,
+		req->analysers);
+
+	if (!(s->flags & SN_ASSIGNED)) {
+		list_for_each_entry(rule, &px->server_rules, list) {
+			int ret;
+
+			ret = acl_exec_cond(rule->cond, s->be, s, &s->txn, ACL_DIR_REQ);
+			ret = acl_pass(ret);
+			if (rule->cond->pol == ACL_COND_UNLESS)
+				ret = !ret;
+
+			if (ret) {
+				struct server *srv = rule->srv.ptr;
+
+				if ((srv->state & SRV_RUNNING) ||
+				    (px->options & PR_O_PERSIST) ||
+				    (s->flags & SN_FORCE_PRST)) {
+					s->flags |= SN_DIRECT | SN_ASSIGNED;
+					set_target_server(&s->target, srv);
+					break;
+				}
+				/* if the server is not UP, let's go on with next rules
+				 * just in case another one is suited.
+				 */
+			}
+		}
+	}
+
+	req->analysers &= ~an_bit;
+	req->analyse_exp = TICK_ETERNITY;
+	return 1;
+}
+
 /* This stream analyser works on a request. It applies all sticking rules on
  * it then returns 1. The data must already be present in the buffer otherwise
  * they won't match. It always returns 1.
@@ -1526,6 +1575,12 @@
 					UPDATE_ANALYSERS(s->req->analysers, ana_list, ana_back, AN_REQ_HTTP_TARPIT);
 				}
 
+				if (ana_list & AN_REQ_SRV_RULES) {
+					if (!process_server_rules(s, s->req, AN_REQ_SRV_RULES))
+						break;
+					UPDATE_ANALYSERS(s->req->analysers, ana_list, ana_back, AN_REQ_SRV_RULES);
+				}
+
 				if (ana_list & AN_REQ_HTTP_INNER) {
 					if (!http_process_request(s, s->req, AN_REQ_HTTP_INNER))
 						break;