MEDIUM: stats: add proxy name filtering on the statistic page

This patch adds a "scope" box in the statistics page in order to
display only proxies with a name that contains the requested value.
The scope filter is preserved across all clicks on the page.
diff --git a/include/common/standard.h b/include/common/standard.h
index 4de4f73..df061b2 100644
--- a/include/common/standard.h
+++ b/include/common/standard.h
@@ -745,4 +745,7 @@
  */
 #define fddebug(msg...) do { char *_m = NULL; memprintf(&_m, ##msg); if (_m) write(-1, _m, strlen(_m)); free(_m); } while (0)
 
+/* same as strstr() but case-insensitive */
+const char *strnistr(const char *str1, int len_str1, const char *str2, int len_str2);
+
 #endif /* _COMMON_STANDARD_H */
diff --git a/include/proto/dumpstats.h b/include/proto/dumpstats.h
index 0674c32..9b41961 100644
--- a/include/proto/dumpstats.h
+++ b/include/proto/dumpstats.h
@@ -54,6 +54,11 @@
 #define STAT_CLI_O_SET  10  /* set entries in tables */
 #define STAT_CLI_O_STAT 11  /* dump stats */
 
+/* HTML form to limit output scope */
+#define STAT_SCOPE_TXT_MAXLEN 20      /* max len for scope substring */
+#define STAT_SCOPE_INPUT_NAME "scope" /* pattern form scope name <input> in html form */
+#define STAT_SCOPE_PATTERN    "?" STAT_SCOPE_INPUT_NAME "="
+
 extern struct si_applet http_stats_applet;
 
 void stats_io_handler(struct stream_interface *si);
diff --git a/include/types/stream_interface.h b/include/types/stream_interface.h
index a808491..2500894 100644
--- a/include/types/stream_interface.h
+++ b/include/types/stream_interface.h
@@ -124,6 +124,8 @@
 				struct proxy *px;
 				struct server *sv;
 				void *l;
+				int scope_str;		/* limit scope to a frontend/backend substring */
+				int scope_len;		/* length of the string above in the buffer */
 				int px_st;		/* STAT_PX_ST* */
 				unsigned int flags;	/* STAT_* */
 				int iid, type, sid;	/* proxy id, type and service id if bounding of stats is enabled */
diff --git a/src/dumpstats.c b/src/dumpstats.c
index 578247e..2069048 100644
--- a/src/dumpstats.c
+++ b/src/dumpstats.c
@@ -2695,11 +2695,25 @@
  */
 static void stats_dump_html_px_hdr(struct stream_interface *si, struct proxy *px, struct uri_auth *uri)
 {
+	char scope_txt[STAT_SCOPE_TXT_MAXLEN + sizeof STAT_SCOPE_PATTERN];
+
 	if (px->cap & PR_CAP_BE && px->srv && (si->applet.ctx.stats.flags & STAT_ADMIN)) {
 		/* A form to enable/disable this proxy servers */
+
+		/* scope_txt = search pattern + search query, si->applet.ctx.stats.scope_len is always <= STAT_SCOPE_TXT_MAXLEN */
+		scope_txt[0] = 0;
+		if (si->applet.ctx.stats.scope_len) {
+			strcpy(scope_txt, STAT_SCOPE_PATTERN);
+			memcpy(scope_txt + strlen(STAT_SCOPE_PATTERN), bo_ptr(si->ob->buf) + si->applet.ctx.stats.scope_str, si->applet.ctx.stats.scope_len);
+			scope_txt[strlen(STAT_SCOPE_PATTERN) + si->applet.ctx.stats.scope_len] = 0;
+		}
+
 		chunk_appendf(&trash,
-			      "<form action=\"%s\" method=\"post\">",
-			      uri->uri_prefix);
+			      "<form action=\"%s%s%s%s\" method=\"post\">",
+			      uri->uri_prefix,
+			      (si->applet.ctx.stats.flags & STAT_HIDE_DOWN) ? ";up" : "",
+			      (si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "",
+			      scope_txt);
 	}
 
 	/* print a new table */
@@ -2768,19 +2782,19 @@
 	if ((px->cap & PR_CAP_BE) && px->srv && (si->applet.ctx.stats.flags & STAT_ADMIN)) {
 		/* close the form used to enable/disable this proxy servers */
 		chunk_appendf(&trash,
-		              "Choose the action to perform on the checked servers : "
-		              "<select name=action>"
-		              "<option value=\"\"></option>"
-		              "<option value=\"disable\">Disable</option>"
-		              "<option value=\"enable\">Enable</option>"
-		              "<option value=\"stop\">Soft Stop</option>"
-		              "<option value=\"start\">Soft Start</option>"
-		              "<option value=\"shutdown\">Kill Sessions</option>"
-		              "</select>"
-		              "<input type=\"hidden\" name=\"b\" value=\"#%d\">"
-		              "&nbsp;<input type=\"submit\" value=\"Apply\">"
-		              "</form>",
-		              px->uuid);
+			      "Choose the action to perform on the checked servers : "
+			      "<select name=action>"
+			      "<option value=\"\"></option>"
+			      "<option value=\"disable\">Disable</option>"
+			      "<option value=\"enable\">Enable</option>"
+			      "<option value=\"stop\">Soft Stop</option>"
+			      "<option value=\"start\">Soft Start</option>"
+			      "<option value=\"shutdown\">Kill Sessions</option>"
+			      "</select>"
+			      "<input type=\"hidden\" name=\"b\" value=\"#%d\">"
+			      "&nbsp;<input type=\"submit\" value=\"Apply\">"
+			      "</form>",
+			      px->uuid);
 	}
 
 	chunk_appendf(&trash, "<p>\n");
@@ -2829,6 +2843,13 @@
 				return 1;
 		}
 
+		/* if the user has requested a limited output and the proxy
+		 * name does not match, skip it.
+		 */
+		if (si->applet.ctx.stats.scope_len &&
+		    strnistr(px->id, strlen(px->id), bo_ptr(si->ob->buf) + si->applet.ctx.stats.scope_str, si->applet.ctx.stats.scope_len) == NULL)
+			return 1;
+
 		if ((si->applet.ctx.stats.flags & STAT_BOUND) &&
 		    (si->applet.ctx.stats.iid != -1) &&
 		    (px->uuid != si->applet.ctx.stats.iid))
@@ -3092,6 +3113,7 @@
 static void stats_dump_html_info(struct stream_interface *si, struct uri_auth *uri)
 {
 	unsigned int up = (now.tv_sec - start_date.tv_sec);
+	char scope_txt[STAT_SCOPE_TXT_MAXLEN + sizeof STAT_SCOPE_PATTERN];
 
 	/* WARNING! this has to fit the first packet too.
 	 * We are around 3.5 kB, add adding entries will
@@ -3147,44 +3169,70 @@
 	              run_queue_cur, nb_tasks_cur, idle_pct
 	              );
 
+	/* scope_txt = search query, si->applet.ctx.stats.scope_len is always <= STAT_SCOPE_TXT_MAXLEN */
+	memcpy(scope_txt, bo_ptr(si->ob->buf) + si->applet.ctx.stats.scope_str, si->applet.ctx.stats.scope_len);
+	scope_txt[si->applet.ctx.stats.scope_len] = '\0';
+
+	chunk_appendf(&trash,
+		      "<li><form method=GET ACTION='%s%s%s'>Scope : <input value='%s' name='" STAT_SCOPE_INPUT_NAME "' autofocus size=8 maxlength='%d'/></form>\n",
+		      uri->uri_prefix,
+		      (si->applet.ctx.stats.flags & STAT_HIDE_DOWN) ? ";up" : "",
+		      (si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "",
+		      (si->applet.ctx.stats.scope_len > 0) ? scope_txt : "",
+		      STAT_SCOPE_TXT_MAXLEN);
+
+	/* scope_txt = search pattern + search query, si->applet.ctx.stats.scope_len is always <= STAT_SCOPE_TXT_MAXLEN */
+	scope_txt[0] = 0;
+	if (si->applet.ctx.stats.scope_len) {
+		strcpy(scope_txt, STAT_SCOPE_PATTERN);
+		memcpy(scope_txt + strlen(STAT_SCOPE_PATTERN), bo_ptr(si->ob->buf) + si->applet.ctx.stats.scope_str, si->applet.ctx.stats.scope_len);
+		scope_txt[strlen(STAT_SCOPE_PATTERN) + si->applet.ctx.stats.scope_len] = 0;
+	}
+
 	if (si->applet.ctx.stats.flags & STAT_HIDE_DOWN)
 		chunk_appendf(&trash,
-		              "<li><a href=\"%s%s%s\">Show all servers</a><br>\n",
+		              "<li><a href=\"%s%s%s%s\">Show all servers</a><br>\n",
 		              uri->uri_prefix,
 		              "",
-		              (si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "");
+		              (si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "",
+			      scope_txt);
 	else
 		chunk_appendf(&trash,
-		              "<li><a href=\"%s%s%s\">Hide 'DOWN' servers</a><br>\n",
+		              "<li><a href=\"%s%s%s%s\">Hide 'DOWN' servers</a><br>\n",
 		              uri->uri_prefix,
 		              ";up",
-		              (si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "");
+		              (si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "",
+			      scope_txt);
 
 	if (uri->refresh > 0) {
 		if (si->applet.ctx.stats.flags & STAT_NO_REFRESH)
 			chunk_appendf(&trash,
-			              "<li><a href=\"%s%s%s\">Enable refresh</a><br>\n",
+			              "<li><a href=\"%s%s%s%s\">Enable refresh</a><br>\n",
 			              uri->uri_prefix,
 			              (si->applet.ctx.stats.flags & STAT_HIDE_DOWN) ? ";up" : "",
-			              "");
+			              "",
+				      scope_txt);
 		else
 			chunk_appendf(&trash,
-			              "<li><a href=\"%s%s%s\">Disable refresh</a><br>\n",
+			              "<li><a href=\"%s%s%s%s\">Disable refresh</a><br>\n",
 			              uri->uri_prefix,
 			              (si->applet.ctx.stats.flags & STAT_HIDE_DOWN) ? ";up" : "",
-			              ";norefresh");
+			              ";norefresh",
+				      scope_txt);
 	}
 
 	chunk_appendf(&trash,
-	              "<li><a href=\"%s%s%s\">Refresh now</a><br>\n",
+	              "<li><a href=\"%s%s%s%s\">Refresh now</a><br>\n",
 	              uri->uri_prefix,
 	              (si->applet.ctx.stats.flags & STAT_HIDE_DOWN) ? ";up" : "",
-	              (si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "");
+	              (si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "",
+		      scope_txt);
 
 	chunk_appendf(&trash,
-	              "<li><a href=\"%s;csv%s\">CSV export</a><br>\n",
+	              "<li><a href=\"%s;csv%s%s\">CSV export</a><br>\n",
 	              uri->uri_prefix,
-	              (uri->refresh > 0) ? ";norefresh" : "");
+	              (uri->refresh > 0) ? ";norefresh" : "",
+		      scope_txt);
 
 	chunk_appendf(&trash,
 	              "</ul></td>"
diff --git a/src/proto_http.c b/src/proto_http.c
index 206701e..610d5a5 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -2971,6 +2971,8 @@
 
 	/* Was the status page requested with a POST ? */
 	if (unlikely(txn->meth == HTTP_METH_POST)) {
+		char scope_txt[STAT_SCOPE_TXT_MAXLEN + sizeof STAT_SCOPE_PATTERN];
+
 		if (si->applet.ctx.stats.flags & STAT_ADMIN) {
 			if (msg->msg_state < HTTP_MSG_100_SENT) {
 				/* If we have HTTP/1.1 and Expect: 100-continue, then we must
@@ -2993,6 +2995,14 @@
 		}
 		else
 			si->applet.ctx.stats.st_code = STAT_STATUS_DENY;
+		/* scope_txt = search pattern + search query, si->applet.ctx.stats.scope_len is always <= STAT_SCOPE_TXT_MAXLEN */
+		scope_txt[0] = 0;
+		if (si->applet.ctx.stats.scope_len) {
+			strcpy(scope_txt, STAT_SCOPE_PATTERN);
+			memcpy(scope_txt + strlen(STAT_SCOPE_PATTERN), bo_ptr(req->buf) + si->applet.ctx.stats.scope_str, si->applet.ctx.stats.scope_len);
+			scope_txt[strlen(STAT_SCOPE_PATTERN) + si->applet.ctx.stats.scope_len] = 0;
+		}
+
 
 		/* We don't want to land on the posted stats page because a refresh will
 		 * repost the data. We don't want this to happen on accident so we redirect
@@ -3003,14 +3013,17 @@
 		             "Cache-Control: no-cache\r\n"
 		             "Content-Type: text/plain\r\n"
 		             "Connection: close\r\n"
-		             "Location: %s;st=%s\r\n"
+		             "Location: %s;st=%s%s%s%s\r\n"
 		             "\r\n",
 		             uri->uri_prefix,
 		             ((si->applet.ctx.stats.st_code > STAT_STATUS_INIT) &&
 		              (si->applet.ctx.stats.st_code < STAT_STATUS_SIZE) &&
 		              stat_status_codes[si->applet.ctx.stats.st_code]) ?
 		             stat_status_codes[si->applet.ctx.stats.st_code] :
-		             stat_status_codes[STAT_STATUS_UNKN]);
+		             stat_status_codes[STAT_STATUS_UNKN],
+			     (si->applet.ctx.stats.flags & STAT_HIDE_DOWN) ? ";up" : "",
+			     (si->applet.ctx.stats.flags & STAT_NO_REFRESH) ? ";norefresh" : "",
+			     scope_txt);
 
 		s->txn.status = 303;
 		s->logs.tv_request = now;
@@ -7827,6 +7840,44 @@
 		}
 		h++;
 	}
+
+	si->applet.ctx.stats.scope_str = 0;
+	si->applet.ctx.stats.scope_len = 0;
+	h = uri + uri_auth->uri_len;
+	while (h <= uri + msg->sl.rq.u_l - 8) {
+		if (memcmp(h, STAT_SCOPE_INPUT_NAME "=", strlen(STAT_SCOPE_INPUT_NAME) + 1) == 0) {
+			int itx = 0;
+			const char *h2;
+			char scope_txt[STAT_SCOPE_TXT_MAXLEN + 1];
+			const char *err;
+
+			h += strlen(STAT_SCOPE_INPUT_NAME) + 1;
+			h2 = h;
+			si->applet.ctx.stats.scope_str = h2 - msg->chn->buf->p;
+			while (*h != ';' && *h != '\0' && *h != '&' && *h != ' ' && *h != '\n') {
+				itx++;
+				h++;
+			}
+
+			if (itx > STAT_SCOPE_TXT_MAXLEN)
+				itx = STAT_SCOPE_TXT_MAXLEN;
+			si->applet.ctx.stats.scope_len = itx;
+
+			/* scope_txt = search query, si->applet.ctx.stats.scope_len is always <= STAT_SCOPE_TXT_MAXLEN */
+			memcpy(scope_txt, h2, itx);
+			scope_txt[itx] = '\0';
+			err = invalid_char(scope_txt);
+			if (err) {
+				/* bad char in search text => clear scope */
+				si->applet.ctx.stats.scope_str = 0;
+				si->applet.ctx.stats.scope_len = 0;
+			}
+			break;
+		}
+		h++;
+	}
+
+
 	return 1;
 }
 
diff --git a/src/standard.c b/src/standard.c
index adeb335..cd60a94 100644
--- a/src/standard.c
+++ b/src/standard.c
@@ -2081,6 +2081,55 @@
 	return out;
 }
 
+
+/* same as strstr() but case-insensitive and with limit length */
+const char *strnistr(const char *str1, int len_str1, const char *str2, int len_str2)
+{
+	char *pptr, *sptr, *start;
+	uint slen, plen;
+	uint tmp1, tmp2;
+
+	if (str1 == NULL || len_str1 == 0) // search pattern into an empty string => search is not found
+		return NULL;
+
+	if (str2 == NULL || len_str2 == 0) // pattern is empty => every str1 match
+		return str1;
+
+	if (len_str1 < len_str2) // pattern is longer than string => search is not found
+		return NULL;
+
+	for (tmp1 = 0, start = (char *)str1, pptr = (char *)str2, slen = len_str1, plen = len_str2; slen >= plen; start++, slen--) {
+		while (toupper(*start) != toupper(*str2)) {
+			start++;
+			slen--;
+			tmp1++;
+
+			if (tmp1 >= len_str1)
+				return NULL;
+
+			/* if pattern longer than string */
+			if (slen < plen)
+				return NULL;
+		}
+
+		sptr = start;
+		pptr = (char *)str2;
+
+		tmp2 = 0;
+		while (toupper(*sptr) == toupper(*pptr)) {
+			sptr++;
+			pptr++;
+			tmp2++;
+
+			if (*pptr == '\0' || tmp2 == len_str2) /* end of pattern found */
+				return start;
+			if (*sptr == '\0' || tmp2 == len_str1) /* end of string found and the pattern is not fully found */
+				return NULL;
+		}
+	}
+	return NULL;
+}
+
 /*
  * Local variables:
  *  c-indent-level: 8