[MEDIUM] session-counters: add HTTP req/err tracking

This patch adds support for the following session counters :
  - http_req_cnt : HTTP request count
  - http_req_rate: HTTP request rate
  - http_err_cnt : HTTP request error count
  - http_err_rate: HTTP request error rate

The equivalent ACLs have been added to check the tracked counters
for the current session or the counters of the current source.
diff --git a/include/proto/session.h b/include/proto/session.h
index 133e6c9..a5dd881 100644
--- a/include/proto/session.h
+++ b/include/proto/session.h
@@ -98,6 +98,45 @@
 	s->term_trace |= code;
 }
 
+/* Increase the number of cumulated HTTP requests in the tracked counters */
+static void inline session_inc_http_req_ctr(struct session *s)
+{
+	if (s->tracked_counters) {
+		void *ptr;
+
+		ptr = stktable_data_ptr(s->tracked_table, s->tracked_counters, STKTABLE_DT_HTTP_REQ_CNT);
+		if (ptr)
+			stktable_data_cast(ptr, http_req_cnt)++;
+
+		ptr = stktable_data_ptr(s->tracked_table, s->tracked_counters, STKTABLE_DT_HTTP_REQ_RATE);
+		if (ptr)
+			update_freq_ctr_period(&stktable_data_cast(ptr, http_req_rate),
+					       s->tracked_table->data_arg[STKTABLE_DT_HTTP_REQ_RATE].u, 1);
+	}
+}
+
+/* Increase the number of cumulated failed HTTP requests in the tracked
+ * counters. Only 4xx requests should be counted here so that we can
+ * distinguish between errors caused by client behaviour and other ones.
+ * Note that even 404 are interesting because they're generally caused by
+ * vulnerability scans.
+ */
+static void inline session_inc_http_err_ctr(struct session *s)
+{
+	if (s->tracked_counters) {
+		void *ptr;
+
+		ptr = stktable_data_ptr(s->tracked_table, s->tracked_counters, STKTABLE_DT_HTTP_ERR_CNT);
+		if (ptr)
+			stktable_data_cast(ptr, http_err_cnt)++;
+
+		ptr = stktable_data_ptr(s->tracked_table, s->tracked_counters, STKTABLE_DT_HTTP_ERR_RATE);
+		if (ptr)
+			update_freq_ctr_period(&stktable_data_cast(ptr, http_err_rate),
+					       s->tracked_table->data_arg[STKTABLE_DT_HTTP_ERR_RATE].u, 1);
+	}
+}
+
 #endif /* _PROTO_SESSION_H */
 
 /*
diff --git a/include/types/stick_table.h b/include/types/stick_table.h
index a1bfc1a..45eac02 100644
--- a/include/types/stick_table.h
+++ b/include/types/stick_table.h
@@ -49,6 +49,10 @@
 	STKTABLE_DT_CONN_CUR,     /* concurrent number of connections */
 	STKTABLE_DT_SESS_CNT,     /* cumulated number of sessions (accepted connections) */
 	STKTABLE_DT_SESS_RATE,    /* accepted sessions rate */
+	STKTABLE_DT_HTTP_REQ_CNT, /* cumulated number of incoming HTTP requests */
+	STKTABLE_DT_HTTP_REQ_RATE,/* incoming HTTP request rate */
+	STKTABLE_DT_HTTP_ERR_CNT, /* cumulated number of HTTP requests errors (4xx) */
+	STKTABLE_DT_HTTP_ERR_RATE,/* HTTP request error rate */
 	STKTABLE_DT_BYTES_IN_CNT, /* cumulated bytes count from client to servers */
 	STKTABLE_DT_BYTES_IN_RATE,/* bytes rate from client to servers */
 	STKTABLE_DT_BYTES_OUT_CNT,/* cumulated bytes count from servers to client */
@@ -72,6 +76,10 @@
 	unsigned int conn_cur;
 	unsigned int sess_cnt;
 	struct freq_ctr_period sess_rate;
+	unsigned int http_req_cnt;
+	struct freq_ctr_period http_req_rate;
+	unsigned int http_err_cnt;
+	struct freq_ctr_period http_err_rate;
 	unsigned long long bytes_in_cnt;
 	struct freq_ctr_period bytes_in_rate;
 	unsigned long long bytes_out_cnt;
diff --git a/src/proto_http.c b/src/proto_http.c
index 57ab14b..7e28762 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -2439,6 +2439,10 @@
 	 * we note the error in the session flags but don't set any state.
 	 * Since the error will be noted there, it will not be counted by
 	 * process_session() as a frontend error.
+	 * Last, we may increase some tracked counters' http request errors on
+	 * the cases that are deliberately the client's fault. For instance,
+	 * a timeout or connection reset is not counted as an error. However
+	 * a bad request is.
 	 */
 
 	if (unlikely(msg->msg_state < HTTP_MSG_BODY)) {
@@ -2446,6 +2450,8 @@
 		 * First, let's catch bad requests.
 		 */
 		if (unlikely(msg->msg_state == HTTP_MSG_ERROR)) {
+			session_inc_http_req_ctr(s);
+			session_inc_http_err_ctr(s);
 			proxy_inc_fe_req_ctr(s->fe);
 			goto return_bad_req;
 		}
@@ -2459,6 +2465,8 @@
 			/* FIXME: check if URI is set and return Status
 			 * 414 Request URI too long instead.
 			 */
+			session_inc_http_req_ctr(s);
+			session_inc_http_err_ctr(s);
 			proxy_inc_fe_req_ctr(s->fe);
 			goto return_bad_req;
 		}
@@ -2472,11 +2480,15 @@
 				goto failed_keep_alive;
 
 			/* we cannot return any message on error */
-			if (msg->err_pos >= 0)
+			if (msg->err_pos >= 0) {
 				http_capture_bad_message(&s->fe->invalid_req, s, req, msg, s->fe);
+				session_inc_http_err_ctr(s);
+			}
+
 			msg->msg_state = HTTP_MSG_ERROR;
 			req->analysers = 0;
 
+			session_inc_http_req_ctr(s);
 			proxy_inc_fe_req_ctr(s->fe);
 			s->fe->counters.failed_req++;
 			if (s->listener->counters)
@@ -2496,13 +2508,16 @@
 				goto failed_keep_alive;
 
 			/* read timeout : give up with an error message. */
-			if (msg->err_pos >= 0)
+			if (msg->err_pos >= 0) {
 				http_capture_bad_message(&s->fe->invalid_req, s, req, msg, s->fe);
+				session_inc_http_err_ctr(s);
+			}
 			txn->status = 408;
 			stream_int_retnclose(req->prod, error_message(s, HTTP_ERR_408));
 			msg->msg_state = HTTP_MSG_ERROR;
 			req->analysers = 0;
 
+			session_inc_http_req_ctr(s);
 			proxy_inc_fe_req_ctr(s->fe);
 			s->fe->counters.failed_req++;
 			if (s->listener->counters)
@@ -2528,6 +2543,8 @@
 			msg->msg_state = HTTP_MSG_ERROR;
 			req->analysers = 0;
 
+			session_inc_http_err_ctr(s);
+			session_inc_http_req_ctr(s);
 			proxy_inc_fe_req_ctr(s->fe);
 			s->fe->counters.failed_req++;
 			if (s->listener->counters)
@@ -2588,6 +2605,7 @@
 	 * left uninitialized (for instance in the absence of headers).
 	 */
 
+	session_inc_http_req_ctr(s);
 	proxy_inc_fe_req_ctr(s->fe); /* one more valid request for this FE */
 
 	if (txn->flags & TX_WAIT_NEXT_RQ) {
@@ -2867,6 +2885,7 @@
 			/* let's log the request time */
 			s->logs.tv_request = now;
 			stream_int_retnclose(req->prod, error_message(s, HTTP_ERR_403));
+			session_inc_http_err_ctr(s);
 			goto return_prx_cond;
 		}
 	}
@@ -2898,6 +2917,7 @@
 			txn->status = 403;
 			s->logs.tv_request = now;
 			stream_int_retnclose(req->prod, error_message(s, HTTP_ERR_403));
+			session_inc_http_err_ctr(s);
 			goto return_prx_cond;
 	}
 
@@ -2913,6 +2933,7 @@
 			/* let's log the request time */
 			s->logs.tv_request = now;
 			stream_int_retnclose(req->prod, error_message(s, HTTP_ERR_403));
+			session_inc_http_err_ctr(s);
 			goto return_prx_cond;
 		}
 
@@ -2932,6 +2953,7 @@
 			req->analyse_exp = tick_add_ifset(now_ms,  s->be->timeout.tarpit);
 			if (!req->analyse_exp)
 				req->analyse_exp = tick_add(now_ms, 0);
+			session_inc_http_err_ctr(s);
 			return 1;
 		}
 	}
@@ -3014,6 +3036,11 @@
 		chunk_initlen(&msg, trash, sizeof(trash), strlen(trash));
 		txn->status = 401;
 		stream_int_retnclose(req->prod, &msg);
+		/* on 401 we still count one error, because normal browsing
+		 * won't significantly increase the counter but brute force
+		 * attempts will.
+		 */
+		session_inc_http_err_ctr(s);
 		goto return_prx_cond;
 	}
 
@@ -3578,8 +3605,10 @@
 
 		if (!ret)
 			goto missing_data;
-		else if (ret < 0)
+		else if (ret < 0) {
+			session_inc_http_err_ctr(s);
 			goto return_bad_req;
+		}
 	}
 
 	/* Now we're in HTTP_MSG_DATA or HTTP_MSG_TRAILERS state.
@@ -3597,8 +3626,10 @@
 
  missing_data:
 	/* we get here if we need to wait for more data */
-	if (req->flags & BF_FULL)
+	if (req->flags & BF_FULL) {
+		session_inc_http_err_ctr(s);
 		goto return_bad_req;
+	}
 
 	if ((req->flags & BF_READ_TIMEOUT) || tick_is_expired(req->analyse_exp, now_ms)) {
 		txn->status = 408;
@@ -4178,8 +4209,10 @@
 
 			if (!ret)
 				goto missing_data;
-			else if (ret < 0)
+			else if (ret < 0) {
+				session_inc_http_err_ctr(s);
 				goto return_bad_req;
+			}
 			/* otherwise we're in HTTP_MSG_DATA or HTTP_MSG_TRAILERS state */
 		}
 		else if (msg->msg_state == HTTP_MSG_DATA_CRLF) {
@@ -4194,8 +4227,10 @@
 
 			if (ret == 0)
 				goto missing_data;
-			else if (ret < 0)
+			else if (ret < 0) {
+				session_inc_http_err_ctr(s);
 				goto return_bad_req;
+			}
 			/* we're in MSG_CHUNK_SIZE now */
 		}
 		else if (msg->msg_state == HTTP_MSG_TRAILERS) {
@@ -4203,8 +4238,10 @@
 
 			if (ret == 0)
 				goto missing_data;
-			else if (ret < 0)
+			else if (ret < 0) {
+				session_inc_http_err_ctr(s);
 				goto return_bad_req;
+			}
 			/* we're in HTTP_MSG_DONE now */
 		}
 		else {
@@ -4512,6 +4549,14 @@
 	n = msg->sol[msg->sl.st.c] - '0';
 	if (n < 1 || n > 5)
 		n = 0;
+	/* when the client triggers a 4xx from the server, it's most often due
+	 * to a missing object or permission. These events should be tracked
+	 * because if they happen often, it may indicate a brute force or a
+	 * vulnerability scan.
+	 */
+	if (n == 4)
+		session_inc_http_err_ctr(s);
+
 	if (s->srv)
 		s->srv->counters.p.http.rsp[n]++;
 
diff --git a/src/session.c b/src/session.c
index aaa0622..1266935 100644
--- a/src/session.c
+++ b/src/session.c
@@ -2483,6 +2483,204 @@
 	return acl_fetch_sess_rate(&px->table, test, stktable_lookup_key(&px->table, key));
 }
 
+/* set test->i to the cumulated number of sessions in the stksess entry <ts> */
+static int
+acl_fetch_http_req_cnt(struct stktable *table, struct acl_test *test, struct stksess *ts)
+{
+	test->flags = ACL_TEST_F_VOL_TEST;
+	test->i = 0;
+	if (ts != NULL) {
+		void *ptr = stktable_data_ptr(table, ts, STKTABLE_DT_HTTP_REQ_CNT);
+		if (!ptr)
+			return 0; /* parameter not stored */
+		test->i = stktable_data_cast(ptr, http_req_cnt);
+	}
+	return 1;
+}
+
+/* set test->i to the cumulated number of sessions from the session's tracked counters */
+static int
+acl_fetch_trk_http_req_cnt(struct proxy *px, struct session *l4, void *l7, int dir,
+		       struct acl_expr *expr, struct acl_test *test)
+{
+	if (!l4->tracked_counters)
+		return 0;
+
+	return acl_fetch_http_req_cnt(l4->tracked_table, test, l4->tracked_counters);
+}
+
+/* set test->i to the cumulated number of session from the session's source
+ * address in the table pointed to by expr.
+ */
+static int
+acl_fetch_src_http_req_cnt(struct proxy *px, struct session *l4, void *l7, int dir,
+		       struct acl_expr *expr, struct acl_test *test)
+{
+	struct stktable_key *key;
+
+	key = tcpv4_src_to_stktable_key(l4);
+	if (!key)
+		return 0; /* only TCPv4 is supported right now */
+
+	if (expr->arg_len)
+		px = find_stktable(expr->arg.str);
+
+	if (!px)
+		return 0; /* table not found */
+
+	return acl_fetch_http_req_cnt(&px->table, test, stktable_lookup_key(&px->table, key));
+}
+
+/* set test->i to the session rate in the stksess entry <ts> over the configured period */
+static int
+acl_fetch_http_req_rate(struct stktable *table, struct acl_test *test, struct stksess *ts)
+{
+	test->flags = ACL_TEST_F_VOL_TEST;
+	test->i = 0;
+	if (ts != NULL) {
+		void *ptr = stktable_data_ptr(table, ts, STKTABLE_DT_HTTP_REQ_RATE);
+		if (!ptr)
+			return 0; /* parameter not stored */
+		test->i = read_freq_ctr_period(&stktable_data_cast(ptr, http_req_rate),
+					       table->data_arg[STKTABLE_DT_HTTP_REQ_RATE].u);
+	}
+	return 1;
+}
+
+/* set test->i to the session rate from the session's tracked counters over
+ * the configured period.
+ */
+static int
+acl_fetch_trk_http_req_rate(struct proxy *px, struct session *l4, void *l7, int dir,
+			struct acl_expr *expr, struct acl_test *test)
+{
+	if (!l4->tracked_counters)
+		return 0;
+
+	return acl_fetch_http_req_rate(l4->tracked_table, test, l4->tracked_counters);
+}
+
+/* set test->i to the session rate from the session's source address in the
+ * table pointed to by expr, over the configured period.
+ */
+static int
+acl_fetch_src_http_req_rate(struct proxy *px, struct session *l4, void *l7, int dir,
+			struct acl_expr *expr, struct acl_test *test)
+{
+	struct stktable_key *key;
+
+	key = tcpv4_src_to_stktable_key(l4);
+	if (!key)
+		return 0; /* only TCPv4 is supported right now */
+
+	if (expr->arg_len)
+		px = find_stktable(expr->arg.str);
+
+	if (!px)
+		return 0; /* table not found */
+
+	return acl_fetch_http_req_rate(&px->table, test, stktable_lookup_key(&px->table, key));
+}
+
+/* set test->i to the cumulated number of sessions in the stksess entry <ts> */
+static int
+acl_fetch_http_err_cnt(struct stktable *table, struct acl_test *test, struct stksess *ts)
+{
+	test->flags = ACL_TEST_F_VOL_TEST;
+	test->i = 0;
+	if (ts != NULL) {
+		void *ptr = stktable_data_ptr(table, ts, STKTABLE_DT_HTTP_ERR_CNT);
+		if (!ptr)
+			return 0; /* parameter not stored */
+		test->i = stktable_data_cast(ptr, http_err_cnt);
+	}
+	return 1;
+}
+
+/* set test->i to the cumulated number of sessions from the session's tracked counters */
+static int
+acl_fetch_trk_http_err_cnt(struct proxy *px, struct session *l4, void *l7, int dir,
+		       struct acl_expr *expr, struct acl_test *test)
+{
+	if (!l4->tracked_counters)
+		return 0;
+
+	return acl_fetch_http_err_cnt(l4->tracked_table, test, l4->tracked_counters);
+}
+
+/* set test->i to the cumulated number of session from the session's source
+ * address in the table pointed to by expr.
+ */
+static int
+acl_fetch_src_http_err_cnt(struct proxy *px, struct session *l4, void *l7, int dir,
+		       struct acl_expr *expr, struct acl_test *test)
+{
+	struct stktable_key *key;
+
+	key = tcpv4_src_to_stktable_key(l4);
+	if (!key)
+		return 0; /* only TCPv4 is supported right now */
+
+	if (expr->arg_len)
+		px = find_stktable(expr->arg.str);
+
+	if (!px)
+		return 0; /* table not found */
+
+	return acl_fetch_http_err_cnt(&px->table, test, stktable_lookup_key(&px->table, key));
+}
+
+/* set test->i to the session rate in the stksess entry <ts> over the configured period */
+static int
+acl_fetch_http_err_rate(struct stktable *table, struct acl_test *test, struct stksess *ts)
+{
+	test->flags = ACL_TEST_F_VOL_TEST;
+	test->i = 0;
+	if (ts != NULL) {
+		void *ptr = stktable_data_ptr(table, ts, STKTABLE_DT_HTTP_ERR_RATE);
+		if (!ptr)
+			return 0; /* parameter not stored */
+		test->i = read_freq_ctr_period(&stktable_data_cast(ptr, http_err_rate),
+					       table->data_arg[STKTABLE_DT_HTTP_ERR_RATE].u);
+	}
+	return 1;
+}
+
+/* set test->i to the session rate from the session's tracked counters over
+ * the configured period.
+ */
+static int
+acl_fetch_trk_http_err_rate(struct proxy *px, struct session *l4, void *l7, int dir,
+			struct acl_expr *expr, struct acl_test *test)
+{
+	if (!l4->tracked_counters)
+		return 0;
+
+	return acl_fetch_http_err_rate(l4->tracked_table, test, l4->tracked_counters);
+}
+
+/* set test->i to the session rate from the session's source address in the
+ * table pointed to by expr, over the configured period.
+ */
+static int
+acl_fetch_src_http_err_rate(struct proxy *px, struct session *l4, void *l7, int dir,
+			struct acl_expr *expr, struct acl_test *test)
+{
+	struct stktable_key *key;
+
+	key = tcpv4_src_to_stktable_key(l4);
+	if (!key)
+		return 0; /* only TCPv4 is supported right now */
+
+	if (expr->arg_len)
+		px = find_stktable(expr->arg.str);
+
+	if (!px)
+		return 0; /* table not found */
+
+	return acl_fetch_http_err_rate(&px->table, test, stktable_lookup_key(&px->table, key));
+}
+
 /* set test->i to the number of kbytes received from clients matching the stksess entry <ts> */
 static int
 acl_fetch_kbytes_in(struct stktable *table, struct acl_test *test, struct stksess *ts)
@@ -2709,6 +2907,14 @@
 	{ "src_sess_cnt",       acl_parse_int,   acl_fetch_src_sess_cnt,      acl_match_int, ACL_USE_TCP4_VOLATILE },
 	{ "trk_sess_rate",      acl_parse_int,   acl_fetch_trk_sess_rate,     acl_match_int, ACL_USE_NOTHING },
 	{ "src_sess_rate",      acl_parse_int,   acl_fetch_src_sess_rate,     acl_match_int, ACL_USE_TCP4_VOLATILE },
+	{ "trk_http_req_cnt",   acl_parse_int,   acl_fetch_trk_http_req_cnt,  acl_match_int, ACL_USE_NOTHING },
+	{ "src_http_req_cnt",   acl_parse_int,   acl_fetch_src_http_req_cnt,  acl_match_int, ACL_USE_TCP4_VOLATILE },
+	{ "trk_http_req_rate",  acl_parse_int,   acl_fetch_trk_http_req_rate, acl_match_int, ACL_USE_NOTHING },
+	{ "src_http_req_rate",  acl_parse_int,   acl_fetch_src_http_req_rate, acl_match_int, ACL_USE_TCP4_VOLATILE },
+	{ "trk_http_err_cnt",   acl_parse_int,   acl_fetch_trk_http_err_cnt,  acl_match_int, ACL_USE_NOTHING },
+	{ "src_http_err_cnt",   acl_parse_int,   acl_fetch_src_http_err_cnt,  acl_match_int, ACL_USE_TCP4_VOLATILE },
+	{ "trk_http_err_rate",  acl_parse_int,   acl_fetch_trk_http_err_rate, acl_match_int, ACL_USE_NOTHING },
+	{ "src_http_err_rate",  acl_parse_int,   acl_fetch_src_http_err_rate, acl_match_int, ACL_USE_TCP4_VOLATILE },
 	{ "trk_kbytes_in",      acl_parse_int,   acl_fetch_trk_kbytes_in,     acl_match_int, ACL_USE_TCP4_VOLATILE },
 	{ "src_kbytes_in",      acl_parse_int,   acl_fetch_src_kbytes_in,     acl_match_int, ACL_USE_TCP4_VOLATILE },
 	{ "trk_bytes_in_rate",  acl_parse_int,   acl_fetch_trk_bytes_in_rate, acl_match_int, ACL_USE_NOTHING },
diff --git a/src/stick_table.c b/src/stick_table.c
index 8b216ae..46d5e1e 100644
--- a/src/stick_table.c
+++ b/src/stick_table.c
@@ -552,6 +552,10 @@
 	[STKTABLE_DT_CONN_CUR]  = { .name = "conn_cur",  .data_length = stktable_data_size(conn_cur)  },
 	[STKTABLE_DT_SESS_CNT]  = { .name = "sess_cnt",  .data_length = stktable_data_size(sess_cnt)  },
 	[STKTABLE_DT_SESS_RATE] = { .name = "sess_rate", .data_length = stktable_data_size(sess_rate), .arg_type = ARG_T_DELAY  },
+	[STKTABLE_DT_HTTP_REQ_CNT]  = { .name = "http_req_cnt",  .data_length = stktable_data_size(http_req_cnt)  },
+	[STKTABLE_DT_HTTP_REQ_RATE] = { .name = "http_req_rate", .data_length = stktable_data_size(http_req_rate), .arg_type = ARG_T_DELAY  },
+	[STKTABLE_DT_HTTP_ERR_CNT]  = { .name = "http_err_cnt",  .data_length = stktable_data_size(http_err_cnt)  },
+	[STKTABLE_DT_HTTP_ERR_RATE] = { .name = "http_err_rate", .data_length = stktable_data_size(http_err_rate), .arg_type = ARG_T_DELAY  },
 	[STKTABLE_DT_BYTES_IN_CNT]  = { .name = "bytes_in_cnt",  .data_length = stktable_data_size(bytes_in_cnt)  },
 	[STKTABLE_DT_BYTES_IN_RATE] = { .name = "bytes_in_rate", .data_length = stktable_data_size(bytes_in_rate), .arg_type = ARG_T_DELAY },
 	[STKTABLE_DT_BYTES_OUT_CNT] = { .name = "bytes_out_cnt", .data_length = stktable_data_size(bytes_out_cnt) },