[MEDIUM] the stats dump FSM was buggy and looped on dispatch instances.

It has been rewritten and now supports an initialization state. It now also
prevents from dumping stopped(disabled) listeners and it is possible to
specify a scope with a list of proxies that are allowed to be dumped from
the one being configured ('.' meaning "this one"). The 'stats' entry can
be configured from the 'defaults' instance and it is correctly flushed
from proxies which redefine it.
diff --git a/ROADMAP b/ROADMAP
index 6faa0e3..b17160c 100644
--- a/ROADMAP
+++ b/ROADMAP
@@ -1,4 +1,4 @@
-'+' = done, '-' = todo
+'+' = done, '-' = todo, '*' = done except doc
 
 1.2.12 :
  + weighted RR/SH
@@ -8,7 +8,15 @@
  + queueing
 
 1.2.14 :
- - HTML status page
+ * HTML status page
+
+        stats enable
+        stats uri /?stats  
+        stats realm w.ods.org\ statistics
+        stats auth user1:pass1
+        stats auth user2:pass2
+        stats auth user3:pass3
+        stats scope <px_id> | '.'
 
  - separate timeout controls
 
@@ -25,6 +33,8 @@
      srv->effective_maxconn =
           max(srv->maxconn * px->nbsess / px->maxconn, srv->minconn)
 
+ - allow server-less proxies (for stats)
+
 1.3 :
  - handle half-closed connections better (cli/srv would not distinguish
    DATA/SHUTR/SHUTW, it would be a session flag which would tell shutr/shutw).
diff --git a/haproxy.c b/haproxy.c
index f18ab6b..e90aae7 100644
--- a/haproxy.c
+++ b/haproxy.c
@@ -442,6 +442,10 @@
 #define DATA_SRC_NONE	0
 #define DATA_SRC_STATS	1
 
+/* data transmission states for the responses */
+#define DATA_ST_INIT	0
+#define DATA_ST_DATA	1
+
 /* different possible states for the client side */
 #define CL_STHEADERS	0
 #define CL_STDATA	1
@@ -641,11 +645,13 @@
 	int status;			/* HTTP status from the server, negative if from proxy */
 	long long bytes;		/* number of bytes transferred from the server */
     } logs;
-    int data_source;			/* where to get the data we generate ourselves */
+    short int data_source;		/* where to get the data we generate ourselves */
+    short int data_state;		/* where to get the data we generate ourselves */
     union {
 	struct {
 	    struct proxy *px;
 	    struct server *sv;
+	    short px_st, sv_st;		/* DATA_ST_INIT or DATA_ST_DATA */
 	} stats;
     } data_ctx;
     unsigned int uniq_id;		/* unique ID used for the traces */
@@ -2981,6 +2987,7 @@
 
 /* writes <len> bytes from message <msg> to buffer <buf>. Returns 0 in case of
  * success, or the number of bytes available otherwise.
+ * FIXME-20060521: handle unaligned data.
  */
 int buffer_write(struct buffer *buf, const char *msg, int len) {
     int max;
@@ -3042,19 +3049,21 @@
     struct proxy *px;
     struct server *sv;
     int msglen;
-    unsigned int up;
-    int failed_checks, down_trans;
 
     if (s->data_source == DATA_SRC_NONE) {
 	s->flags &= ~SN_SELF_GEN;
 	return 1;
     }
     else if (s->data_source == DATA_SRC_STATS) {
-	if (s->data_ctx.stats.px == NULL) { /* initialized to NULL at session creation */
-	    /* the proxy was not known, the function had not been called yet */
-	    
+	msglen = 0;
+
+	if (s->data_state == DATA_ST_INIT) { /* the function had not been called yet */
+	    unsigned int up;
+
 	    s->flags |= SN_SELF_GEN;  // more data will follow
-	    msglen = sprintf(trash,
+
+	    /* send the start of the HTTP response */
+	    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
 			     "HTTP/1.0 200 OK\r\n"
 			     "Cache-Control: no-cache\r\n"
 			     "Connection: close\r\n"
@@ -3062,13 +3071,15 @@
 	    
 	    s->logs.status = 200;
 	    client_retnclose(s, msglen, trash); // send the start of the response.
+	    msglen = 0;
+
 	    if (!(s->flags & SN_ERR_MASK))  // this is not really an error but it is
 		    s->flags |= SN_ERR_PRXCOND; // to mark that it comes from the proxy
 	    if (!(s->flags & SN_FINST_MASK))
 		s->flags |= SN_FINST_R;
 
 	    /* WARNING! This must fit in the first buffer !!! */	    
-	    msglen = snprintf(trash, sizeof(trash),
+	    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
 			     "<html><head><title>Statistics Report for " PRODUCT_NAME "</title>\n"
 			     "<meta http-equiv=\"content-type\" content=\"text/html; charset=iso-8859-1\">\n"
 			     "<style type=\"text/css\"><!--\n"
@@ -3124,11 +3135,12 @@
 
 	    if (buffer_write(rep, trash, msglen) != 0)
 		return 0;
+	    msglen = 0;
 
 	    up = (now.tv_sec - start_date.tv_sec);
 
 	    /* WARNING! this has to fit the first packet too */
-	    msglen = snprintf(trash, sizeof(trash),
+	    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
 			      "<body><h1>" PRODUCT_NAME "</h1>\n"
 			      "<h2>Statistics Report for pid %d</h2>\n"
 			      "<hr width=\"100%%\" class=\"hr\">\n"
@@ -3166,18 +3178,52 @@
 	    
 	    if (buffer_write(rep, trash, msglen) != 0)
 		return 0;
+	    msglen = 0;
+
+	    s->data_state = DATA_ST_DATA;
+	    memset(&s->data_ctx, 0, sizeof(s->data_ctx));
+
 	    px = s->data_ctx.stats.px = proxy;
+	    s->data_ctx.stats.px_st = DATA_ST_INIT;
 	}
 
 	while (s->data_ctx.stats.px) {
 	    int dispatch_sess, dispatch_cum;
+	    int failed_checks, down_trans;
 
-	    /* if sv == NULL, this is because we are on a new proxy :
-	     * initialized to NULL at session creation
-	     */
-	    while (s->data_ctx.stats.sv == NULL) {
+	    if (s->data_ctx.stats.px_st == DATA_ST_INIT) {
+		/* we are on a new proxy */
 		px = s->data_ctx.stats.px;
-		msglen = snprintf(trash, sizeof(trash),
+
+		/* skip the disabled proxies */
+		if (px->state == PR_STSTOPPED)
+		    goto next_proxy;
+
+		if (s->proxy->uri_auth && s->proxy->uri_auth->scope) {
+		    /* we have a limited scope, we have to check the proxy name */
+		    struct stat_scope *scope;
+		    int len;
+
+		    len = strlen(px->id);
+		    scope = s->proxy->uri_auth->scope;
+
+		    while (scope) {
+			/* match exact proxy name */
+			if (scope->px_len == len && !memcmp(px->id, scope->px_id, len))
+			    break;
+
+			/* match '.' which means 'self' proxy */
+			if (!strcmp(scope->px_id, ".") && px == s->proxy)
+			    break;
+			scope = scope->next;
+		    }
+
+		    /* proxy name not found */
+		    if (scope == NULL)
+			goto next_proxy;
+		}
+
+		msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
 				  "<h3>&gt; Proxy instance %s : "
 				  "%d conns (maxconn=%d), %d queued (%d unassigned), %d total conns</h3>\n"
 				  "",
@@ -3199,29 +3245,23 @@
 		
 		if (buffer_write(rep, trash, msglen) != 0)
 		    return 0;
+		msglen = 0;
+
 		s->data_ctx.stats.sv = px->srv;
+		s->data_ctx.stats.px_st = DATA_ST_DATA;
 	    }
 
 	    px = s->data_ctx.stats.px;
 
-	    dispatch_sess = px->nbconn;
-	    dispatch_cum = px->cum_conn;
-	    failed_checks = down_trans = 0;
+	    /* stats.sv has been initialized above */
 	    while (s->data_ctx.stats.sv != NULL) {
 		static char *act_tab_bg[5] = { /*down*/"#FF9090", /*rising*/"#FFD020", /*failing*/"#FFFFA0", /*up*/"#C0FFC0", /*unchecked*/"#E0E0E0" };
 		static char *bck_tab_bg[5] = { /*down*/"#FF9090", /*rising*/"#FF80ff", /*failing*/"#C060FF", /*up*/"#B0D0FF", /*unchecked*/"#E0E0E0" };
 		static char *srv_hlt_st[5] = { "DOWN", "DN %d/%d &uarr;", "UP %d/%d &darr;", "UP", "<i>no check</i>" };
 		int sv_state; /* 0=DOWN, 1=going up, 2=going down, 3=UP */
 
-		px = s->data_ctx.stats.px;
 		sv = s->data_ctx.stats.sv;
 
-		dispatch_sess -= sv->cur_sess;
-		dispatch_cum -= sv->cum_sess;
-
-		failed_checks += sv->failed_checks;
-		down_trans += sv->down_trans;
-
 		/* FIXME: produce some small strings for "UP/DOWN x/y &#xxxx;" */
 		if (!(sv->state & SRV_CHECKED))
 		    sv_state = 4;
@@ -3237,7 +3277,7 @@
 			sv_state = 0; /* DOWN */
 
 		/* name, weight */
-		msglen = snprintf(trash, sizeof(trash),
+		msglen += snprintf(trash, sizeof(trash),
 				  "<tr align=center bgcolor=\"%s\"><td>%s</td><td>%d</td><td>",
 				  (sv->state & SRV_BACKUP) ? bck_tab_bg[sv_state] : act_tab_bg[sv_state],
 				  sv->id, sv->uweight+1);
@@ -3271,66 +3311,88 @@
 		    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
 				       "<td align=right>-</td><td align=right>-</td></tr>\n");
 
+		if (buffer_write(rep, trash, msglen) != 0)
+		    return 0;
+		msglen = 0;
+
+		s->data_ctx.stats.sv = sv->next;
+	    } /* while sv */
+
+	    /* now we are past the last server, we'll dump information about the dispatcher */
+
+	    /* We have to count down from the proxy to the servers to tell how
+	     * many sessions are on the dispatcher, and how many checks have
+	     * failed. We cannot count this during the servers dump because it
+	     * might be interrupted multiple times.
+	     */
+	    dispatch_sess = px->nbconn;
+	    dispatch_cum = px->cum_conn;
+	    failed_checks = down_trans = 0;
+
+	    sv = px->srv;
+	    while (sv) {
+		dispatch_sess -= sv->cur_sess;
+		dispatch_cum  -= sv->cum_sess;
+		failed_checks += sv->failed_checks;
+		down_trans    += sv->down_trans;
 		sv = sv->next;
-		if (!sv) {
-		    /* first, write informations about the dispatcher */
-		    /* name, weight, status, act, bck */
-		    msglen += snprintf(trash + msglen, sizeof(trash),
-				      "<tr align=center bgcolor=\"#e8e8d0\">"
-				      "<td>Dispatcher</td><td>-</td>"
-				      "<td>%s</td><td>-</td><td>-</td>",
-				      px->state == PR_STRUN ? "UP" : "DOWN");
+	    }
 
-		    /* queue : current, max */
-		    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
-				       "<td align=right>%d</td><td align=right>%d</td>",
-				       px->nbpend, px->nbpend_max);
+	    /* name, weight, status, act, bck */
+	    msglen += snprintf(trash + msglen, sizeof(trash),
+			       "<tr align=center bgcolor=\"#e8e8d0\">"
+			       "<td>Dispatcher</td><td>-</td>"
+			       "<td>%s</td><td>-</td><td>-</td>",
+			       px->state == PR_STRUN ? "UP" : "DOWN");
 
-		    /* sessions : current, max, limit, cumul */
-		    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
-				       "<td align=right>%d</td><td align=right>%d</td><td align=right>%d</td><td align=right>%d</td>",
-				       dispatch_sess, px->nbconn_max, px->maxconn, dispatch_cum);
+	    /* queue : current, max */
+	    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
+			       "<td align=right>%d</td><td align=right>%d</td>",
+			       px->nbpend, px->nbpend_max);
 
-		    /* failures : unique, fatal */
-		    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
-				       "<td align=right>-</td><td align=right>-</td></tr>\n");
+	    /* sessions : current, max, limit, cumul. */
+	    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
+			       "<td align=right>%d</td><td align=right>%d</td><td align=right>%d</td><td align=right>%d</td>",
+			       dispatch_sess, px->nbconn_max, px->maxconn, dispatch_cum);
 
+	    /* failures : unique, fatal */
+	    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
+			       "<td align=right>-</td><td align=right>-</td></tr>\n");
 
-		    /* now the summary for the proxy */
-		    /* name, weight, status, act, bck */
-		    msglen += snprintf(trash + msglen, sizeof(trash),
-				      "<tr align=center style=\"color: #ffff80;  background: #20C0C0;\">"
-				      "<td><b>Total</b></td><td>-</td>"
-				      "<td><b>%s</b></td><td><b>%d</b></td><td><b>%d</b></td>",
-				      (px->state == PR_STRUN && (px->srv_act || px->srv_bck)) ? "UP" : "DOWN",
-				      px->srv_act, px->srv_bck);
 
-		    /* queue : current, max */
-		    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
-				       "<td align=right><b>%d</b></td><td align=right><b>%d</b></td>",
-				       px->totpend, px->nbpend_max);
+	    /* now the summary for the whole proxy */
+	    /* name, weight, status, act, bck */
+	    msglen += snprintf(trash + msglen, sizeof(trash),
+			       "<tr align=center style=\"color: #ffff80;  background: #20C0C0;\">"
+			       "<td><b>Total</b></td><td>-</td>"
+			       "<td><b>%s</b></td><td><b>%d</b></td><td><b>%d</b></td>",
+			       (px->state == PR_STRUN && ((px->srv == NULL) || px->srv_act || px->srv_bck)) ? "UP" : "DOWN",
+			       px->srv_act, px->srv_bck);
 
-		    /* sessions : current, max, limit, cumul */
-		    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
-				       "<td align=right><b>%d</b></td><td align=right><b>%d</b></td><td align=right><b>%d</b></td><td align=right><b>%d</b></td>",
-				       px->nbconn, px->nbconn_max, px->maxconn, px->cum_conn);
+	    /* queue : current, max */
+	    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
+			       "<td align=right><b>%d</b></td><td align=right><b>%d</b></td>",
+			       px->totpend, px->nbpend_max);
 
-		    /* failures : unique, fatal */
-		    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
-				       "<td align=right>%d</td><td align=right>%d</td></tr>\n",
-				       failed_checks, down_trans);
+	    /* sessions : current, max, limit, cumul */
+	    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
+			       "<td align=right><b>%d</b></td><td align=right><b>%d</b></td><td align=right><b>%d</b></td><td align=right><b>%d</b></td>",
+			       px->nbconn, px->nbconn_max, px->maxconn, px->cum_conn);
 
-		    msglen += snprintf(trash + msglen, sizeof(trash) - msglen, "</table><p>\n");
+	    /* failures : unique, fatal */
+	    msglen += snprintf(trash + msglen, sizeof(trash) - msglen,
+			       "<td align=right>%d</td><td align=right>%d</td></tr>\n",
+			       failed_checks, down_trans);
 
-		    px = px->next;
-		    /* the server loop will automatically detect the NULL */
-		}
-		
-		if (buffer_write(rep, trash, msglen) != 0)
-		    return 0;
-		s->data_ctx.stats.sv = sv;
-		s->data_ctx.stats.px = px;
-	    } /* server loop */
+	    msglen += snprintf(trash + msglen, sizeof(trash) - msglen, "</table><p>\n");
+
+	    if (buffer_write(rep, trash, msglen) != 0)
+		return 0;
+	    msglen = 0;
+	    
+	    s->data_ctx.stats.px_st = DATA_ST_INIT;
+	next_proxy:
+	    s->data_ctx.stats.px = px->next;
 	} /* proxy loop */
 	/* here, we just have reached the sv == NULL and px == NULL */
 	s->flags &= ~SN_SELF_GEN;
@@ -3611,7 +3673,6 @@
 	s->logs.srv_queue_size = 0; /* we will get this number soon */
 
 	s->data_source = DATA_SRC_NONE;
-	memset(&s->data_ctx, 0, sizeof(s->data_ctx));
 
 	s->uniq_id = totalconn;
 	p->cum_conn++;
@@ -4193,6 +4254,7 @@
 			req->rlim = req->data + BUFSIZE; /* no more rewrite needed */
 			t->logs.t_request = tv_diff(&t->logs.tv_accept, &now);
 			t->data_source = DATA_SRC_STATS;
+			t->data_state  = DATA_ST_INIT;
 			produce_content(t);
 			return 1;
 		    }
@@ -7827,6 +7889,7 @@
 	curproxy->loglev2 = defproxy.loglev2;
 	curproxy->to_log = defproxy.to_log & ~LW_COOKIE & ~LW_REQHDR & ~ LW_RSPHDR;
 	curproxy->grace  = defproxy.grace;
+	curproxy->uri_auth  = defproxy.uri_auth;
 	curproxy->source_addr = defproxy.source_addr;
 	curproxy->mon_net = defproxy.mon_net;
 	curproxy->mon_mask = defproxy.mon_mask;
@@ -7844,7 +7907,7 @@
 	if (defproxy.errmsg.msg502) free(defproxy.errmsg.msg502);
 	if (defproxy.errmsg.msg503) free(defproxy.errmsg.msg503);
 	if (defproxy.errmsg.msg504) free(defproxy.errmsg.msg504);
-
+	/* we cannot free uri_auth because it might already be used */
 	init_default_instance();
 	curproxy = &defproxy;
 	return 0;
@@ -8112,8 +8175,11 @@
 	curproxy->conn_retries = atol(args[1]);
     }
     else if (!strcmp(args[0], "stats")) {
+	if (curproxy != &defproxy && curproxy->uri_auth == defproxy.uri_auth)
+	    curproxy->uri_auth = NULL; /* we must detach from the default config */
+
 	if (*(args[1]) == 0) {
-	    Alert("parsing [%s:%d] : '%s' expects 'uri', 'realm', 'auth' or 'enable'.\n", file, linenum, args[0]);
+	    Alert("parsing [%s:%d] : '%s' expects 'uri', 'realm', 'auth', 'scope' or 'enable'.\n", file, linenum, args[0]);
 	    return -1;
 	} else if (!strcmp(args[1], "uri")) {
 	    if (*(args[2]) == 0) {
@@ -8139,6 +8205,14 @@
 		Alert("parsing [%s:%d] : out of memory.\n", file, linenum);
 		return -1;
 	    }
+	} else if (!strcmp(args[1], "scope")) {
+	    if (*(args[2]) == 0) {
+		Alert("parsing [%s:%d] : 'scope' needs a proxy name.\n", file, linenum);
+		return -1;
+	    } else if (!stats_add_scope(&curproxy->uri_auth, args[2])) {
+		Alert("parsing [%s:%d] : out of memory.\n", file, linenum);
+		return -1;
+	    }
 	} else if (!strcmp(args[1], "enable")) {
 	    if (!stats_check_init_uri_auth(&curproxy->uri_auth)) {
 		Alert("parsing [%s:%d] : out of memory.\n", file, linenum);
diff --git a/include/uri_auth.h b/include/uri_auth.h
index f7ff6c8..80149ca 100644
--- a/include/uri_auth.h
+++ b/include/uri_auth.h
@@ -19,12 +19,22 @@
 	char *user_pwd;			/* auth as base64("user":"passwd") (see RFC2617) */
 };
 
+/* This is a list of proxies we are allowed to see. Later, it should go in the
+ * user list, but before this we need to support de/re-authentication.
+ */
+struct stat_scope {
+	struct stat_scope *next;	/* next entry, NULL if none */
+	int px_len;			/* proxy name length */
+	char *px_id;			/* proxy id */
+};
+
 /* later we may link them to support multiple URI matching */
 struct uri_auth {
 	int uri_len;			/* the prefix length */
 	char *uri_prefix;		/* the prefix we want to match */
 	char *auth_realm;		/* the realm reported to the client */
 	struct user_auth *users;	/* linked list of valid user:passwd couples */
+	struct stat_scope *scope;	/* linked list of authorized proxies */
 };
 
 /* This is the default statistics URI */
@@ -53,5 +63,6 @@
 struct uri_auth *stats_set_uri(struct uri_auth **root, char *uri);
 struct uri_auth *stats_set_realm(struct uri_auth **root, char *realm);
 struct uri_auth *stats_add_auth(struct uri_auth **root, char *user);
+struct uri_auth *stats_add_scope(struct uri_auth **root, char *scope);
 
 #endif /* URI_AUTH_H */
diff --git a/src/uri_auth.c b/src/uri_auth.c
index 3c4965e..84570ef 100644
--- a/src/uri_auth.c
+++ b/src/uri_auth.c
@@ -160,3 +160,45 @@
 	return NULL;
 }
 
+/*
+ * Returns a default uri_auth with a <scope> entry added to the list of
+ * allowed scopes. If a matching entry is found, no update will be performed.
+ * Uses the pointer provided if not NULL and not initialized.
+ */
+struct uri_auth *stats_add_scope(struct uri_auth **root, char *scope)
+{
+	struct uri_auth *u;
+	char *new_name;
+	struct stat_scope *old_scope, **scope_list;
+
+	if ((u = stats_check_init_uri_auth(root)) == NULL)
+		goto out;
+
+	scope_list = &u->scope;
+	while ((old_scope = *scope_list)) {
+		if (!strcmp(old_scope->px_id, scope))
+			break;
+		scope_list = &old_scope->next;
+	}
+
+	if (!old_scope) {
+		if ((new_name = strdup(scope)) == NULL)
+			goto out_u;
+
+		if ((old_scope = (struct stat_scope *)calloc(1, sizeof(*old_scope))) == NULL)
+			goto out_name;
+
+		old_scope->px_id = new_name;
+		old_scope->px_len = strlen(new_name);
+		*scope_list = old_scope;
+	}
+	return u;
+
+ out_name:
+	free(new_name);
+ out_u:
+	free(u);
+ out:
+	return NULL;
+}
+