MINOR: checks: Add a way to send custom headers and payload during http chekcs

The 'http-check send' directive have been added to add headers and optionnaly a
payload to the request sent during HTTP healthchecks. The request line may be
customized by the "option httpchk" directive but there was not official way to
add extra headers. An old trick consisted to hide these headers at the end of
the version string, on the "option httpchk" line. And it was impossible to add
an extra payload with an "http-check expect" directive because of the
"Connection: close" header appended to the request (See issue #16 for details).

So to make things official and fully support payload additions, the "http-check
send" directive have been added :

    option httpchk POST /status HTTP/1.1

    http-check send hdr Content-Type "application/json;charset=UTF-8" \
        hdr X-test-1 value1 hdr X-test-2 value2 \
        body "{id: 1, field: \"value\"}"

When a payload is defined, the Content-Length header is automatically added. So
chunk-encoded requests are not supported yet. For now, there is no special
validity checks on the extra headers.

This patch is inspired by Kiran Gavali's work. It should fix the issue #16 and
as far as possible, it may be backported, at least as far as 1.8.

(cherry picked from commit 8acb1284bcd47301fce0aa45a7083dbcef94cb5f)
Signed-off-by: Willy Tarreau <w@1wt.eu>
(cherry picked from commit 104d66a2caa12ba8eb528320e2e9b0136d754ccd)
Signed-off-by: Willy Tarreau <w@1wt.eu>
diff --git a/doc/configuration.txt b/doc/configuration.txt
index f8766f6..ebbc6fd 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -4213,6 +4213,33 @@
   See also : "option httpchk", "http-check disable-on-404"
 
 
+http-check send [hdr <name> <value>]* [body <string>]
+  Add a possible list of headers and/or a body to the request sent during HTTP
+  health checks.
+  May be used in sections :   defaults | frontend | listen | backend
+                                 yes   |    no    |   yes  |   yes
+  Arguments :
+    hdr <name> <value>   adds the HTTP header field whose name is specified in
+                         <name> and whose value is defined by <value> to the
+                         request sent during HTTP health checks.
+
+    body <string>        add the body defined by <string> to the request sent
+                         sent during HTTP health checks. If defined, the
+                         "Content-Length" header is thus automatically added
+                         to the request.
+
+  In addition to the request line defined by the "option httpchk" directive,
+  this one is the valid way to add some headers and optionally a body to the
+  request sent during HTTP health checks. If a body is defined, the associate
+  "Content-Length" header is automatically added. The old trick consisting to
+  add headers after the version string on the "option httpchk" line is now
+  deprecated. Note also the "Connection: close" header is still added if a
+  "http-check expect" direcive is defined independently of this directive, just
+  like the state header if the directive "http-check send-state" is defined.
+
+  See also : "option httpchk", "http-check send-state" and "http-check expect"
+
+
 http-check send-state
   Enable emission of a state header with HTTP health checks
   May be used in sections :   defaults | frontend | listen | backend
@@ -6754,8 +6781,7 @@
     <version> is the optional HTTP version string. It defaults to "HTTP/1.0"
               but some servers might behave incorrectly in HTTP 1.0, so turning
               it to HTTP/1.1 may sometimes help. Note that the Host field is
-              mandatory in HTTP/1.1, and as a trick, it is possible to pass it
-              after "\r\n" following the version string.
+              mandatory in HTTP/1.1, use "http-check send" directive to add it.
 
   By default, server health checks only consist in trying to establish a TCP
   connection. When "option httpchk" is specified, a complete HTTP request is
@@ -6769,12 +6795,18 @@
   plain TCP backends. This is particularly useful to check simple scripts bound
   to some dedicated ports using the inetd daemon.
 
+  Note : For a while, there was no way to add headers or body in the request
+         used for HTTP health checks. So a workaround was to hide it at the end
+         of the version string with a "\r\n" after the version. It is now
+	 deprecated. The directive "http-check send" must be used instead.
+
   Examples :
       # Relay HTTPS traffic to Apache instance and check service availability
       # using HTTP request "OPTIONS * HTTP/1.1" on port 80.
       backend https_relay
           mode tcp
-          option httpchk OPTIONS * HTTP/1.1\r\nHost:\ www
+          option httpchk OPTIONS * HTTP/1.1
+          http-check send hdr Host www
           server apache1 192.168.1.1:443 check port 80
 
   See also : "option ssl-hello-chk", "option smtpchk", "option mysql-check",
diff --git a/include/types/proxy.h b/include/types/proxy.h
index 9b06955..06887c3 100644
--- a/include/types/proxy.h
+++ b/include/types/proxy.h
@@ -428,6 +428,10 @@
 	int grace;				/* grace time after stop request */
 	int check_len;				/* Length of the HTTP or SSL3 request */
 	char *check_req;			/* HTTP or SSL request to use for PR_O_HTTP_CHK|PR_O_SSL3_CHK */
+	int check_body_len;                     /* Length of the request body for HTTP checks */
+	char *check_hdrs;                       /* Request headers for HTTP cheks */
+	int check_hdrs_len;                     /* Length of the headers for HTTP checks */
+	char *check_body;                       /* Request body for HTTP cheks */
 	char *check_command;			/* Command to use for external agent checks */
 	char *check_path;			/* PATH environment to use for external agent checks */
 	char *expect_str;			/* http-check expected content : string or text version of the regex */
diff --git a/reg-tests/checks/http-check-send.vtc b/reg-tests/checks/http-check-send.vtc
new file mode 100644
index 0000000..7e2e37b
--- /dev/null
+++ b/reg-tests/checks/http-check-send.vtc
@@ -0,0 +1,150 @@
+varnishtest "Health-checks: http-check send test"
+
+feature ignore_unknown_macro
+
+# This script tests HTTP health-checks and more particularly the "http-check
+# send" directive.
+
+server s1 {
+    rxreq
+    expect req.method == OPTIONS
+    expect req.url == /
+    expect req.proto == HTTP/1.0
+    txresp
+} -start
+
+server s2 {
+    rxreq
+    expect req.method == GET
+    expect req.url == /test
+    expect req.proto == HTTP/1.1
+    txresp
+} -start
+
+server s3 {
+    rxreq
+    expect req.method == OPTIONS
+    expect req.url == /
+    expect req.proto == HTTP/1.0
+    expect req.http.hdr == <undef>
+    expect req.http.host == <undef>
+    expect req.http.x-test == <undef>
+    expect req.http.content-length == <undef>
+    expect req.bodylen == 0
+    txresp
+} -start
+
+server s4 {
+    rxreq
+    expect req.method == GET
+    expect req.url == /status
+    expect req.proto == HTTP/1.1
+    expect req.http.hdr == <undef>
+    expect req.http.host == "my-www-host"
+    expect req.http.x-test == true
+    expect req.http.content-length == 4
+    expect req.bodylen == 4
+    expect req.body == "test"
+    txresp
+} -start
+
+server s5 {
+    rxreq
+    expect req.method == GET
+    expect req.url == /status
+    expect req.proto == HTTP/1.1
+    expect req.http.hdr == <undef>
+    expect req.http.host == "other-www-host"
+    expect req.http.x-test == <undef>
+    expect req.http.x-new-test == true
+    expect req.http.content-length == 10
+    expect req.bodylen == 10
+    expect req.body == "other test"
+    txresp
+} -start
+
+
+syslog S1 -level notice {
+    recv
+    expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Proxy be1 started."
+    recv
+    expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Health check for server be1/srv succeeded.*code: 200"
+} -start
+
+syslog S2 -level notice {
+    recv
+    expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Proxy be2 started."
+    recv
+    expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Health check for server be2/srv succeeded.*code: 200"
+} -start
+
+syslog S3 -level notice {
+    recv
+    expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Proxy be3 started."
+    recv
+    expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Health check for server be3/srv succeeded.*code: 200"
+} -start
+
+syslog S4 -level notice {
+    recv
+    expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Proxy be4 started."
+    recv
+    expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Health check for server be4/srv succeeded.*code: 200"
+} -start
+
+syslog S5 -level notice {
+    recv
+    expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Proxy be5 started."
+    recv
+    expect ~ "[^:\\[ ]\\[${h1_pid}\\]: Health check for server be5/srv succeeded.*code: 200"
+} -start
+
+
+haproxy h1 -conf {
+    defaults
+        mode http
+        timeout client 1s
+        timeout server 1s
+        timeout connect 200ms
+        option httpchk
+        option log-health-checks
+
+    backend be1
+        log ${S1_addr}:${S1_port} len 2048 local0
+        server srv ${s1_addr}:${s1_port} check inter 200ms rise 1 fall 1
+
+    backend be2
+        log ${S2_addr}:${S2_port} len 2048 local0
+        option httpchk GET /test HTTP/1.1
+        server srv ${s2_addr}:${s2_port} check inter 200ms rise 1 fall 1
+
+    defaults
+        mode http
+        timeout client 1s
+        timeout server 1s
+        timeout connect 200ms
+        option httpchk GET /status HTTP/1.1\r\nHdr:\ must-be-removed
+        option log-health-checks
+        http-check send hdr Host "my-www-host" hdr X-test true body "test"
+
+    backend be3
+        option httpchk
+        log ${S3_addr}:${S3_port} len 2048 local0
+        server srv ${s3_addr}:${s3_port} check inter 200ms rise 1 fall 1
+
+    backend be4
+        log ${S4_addr}:${S4_port} len 2048 local0
+        server srv ${s4_addr}:${s4_port} check inter 200ms rise 1 fall 1
+
+    backend be5
+        log ${S5_addr}:${S5_port} len 2058 local0
+        http-check send hdr Host "other-www-host" hdr X-New-Test true body "other test"
+        server srv ${s5_addr}:${s5_port} check inter 200ms rise 1 fall 1
+
+} -start
+
+syslog S1 -wait
+syslog S2 -wait
+syslog S3 -wait
+syslog S4 -wait
+syslog S5 -wait
diff --git a/src/cfgparse-listen.c b/src/cfgparse-listen.c
index de453f6..585f514 100644
--- a/src/cfgparse-listen.c
+++ b/src/cfgparse-listen.c
@@ -438,6 +438,18 @@
 			}
 			curproxy->check_len = defproxy.check_len;
 
+			if (defproxy.check_hdrs) {
+				curproxy->check_hdrs = calloc(1, defproxy.check_hdrs_len);
+				memcpy(curproxy->check_hdrs, defproxy.check_hdrs, defproxy.check_hdrs_len);
+			}
+			curproxy->check_hdrs_len = defproxy.check_hdrs_len;
+
+			if (defproxy.check_body) {
+				curproxy->check_body = calloc(1, defproxy.check_body_len);
+				memcpy(curproxy->check_body, defproxy.check_body, defproxy.check_body_len);
+			}
+			curproxy->check_body_len = defproxy.check_body_len;
+
 			if (defproxy.expect_str) {
 				curproxy->expect_str = strdup(defproxy.expect_str);
 				if (defproxy.expect_regex) {
@@ -2443,7 +2455,10 @@
 
 			/* use HTTP request to check servers' health */
 			free(curproxy->check_req);
-			curproxy->check_req = NULL;
+			free(curproxy->check_hdrs);
+			free(curproxy->check_body);
+			curproxy->check_req = curproxy->check_hdrs = curproxy->check_body = NULL;
+			curproxy->check_len = curproxy->check_hdrs_len = curproxy->check_body_len = 0;
 			curproxy->options2 &= ~PR_O2_CHK_ANY;
 			curproxy->options2 |= PR_O2_HTTP_CHK;
 			if (!*args[2]) { /* no argument */
@@ -2454,16 +2469,49 @@
 				curproxy->check_req = malloc(reqlen);
 				curproxy->check_len = snprintf(curproxy->check_req, reqlen,
 							       "OPTIONS %s HTTP/1.0\r\n", args[2]); /* URI to use */
-			} else { /* more arguments : METHOD URI [HTTP_VER] */
-				int reqlen = strlen(args[2]) + strlen(args[3]) + 3 + strlen("\r\n");
-				if (*args[4])
-					reqlen += strlen(args[4]);
-				else
-					reqlen += strlen("HTTP/1.0");
+			} else if (!*args[4]) { /* two arguments : METHOD URI */
+				int reqlen = strlen(args[2]) + strlen(args[3]) + strlen(" HTTP/1.0\r\n") + 1;
 
 				curproxy->check_req = malloc(reqlen);
 				curproxy->check_len = snprintf(curproxy->check_req, reqlen,
-							       "%s %s %s\r\n", args[2], args[3], *args[4]?args[4]:"HTTP/1.0");
+							       "%s %s HTTP/1.0\r\n", args[2], args[3]);
+			} else { /* 3 arguments : METHOD URI HTTP_VER */
+				char *vsn = args[4];
+				char *hdrs = strstr(vsn, "\r\n");
+				char *body = strstr(vsn, "\r\n\r\n");
+
+				if (hdrs || body) {
+					ha_warning("parsing [%s:%d]: '%s %s' : hiding headers or body at the end of the version string is deprecated."
+						   " Please, consider to use 'http-check send' directive instead.\n",
+						   file, linenum, args[0], args[1]);
+					err_code |= ERR_WARN;
+				}
+
+				if (hdrs == body)
+					hdrs = NULL;
+				if (hdrs) {
+					*hdrs = '\0';
+					hdrs += 2;
+				}
+				if (body) {
+					*body = '\0';
+					body += 4;
+				}
+
+				curproxy->check_len = strlen(args[2]) + strlen(args[3]) + strlen(vsn) + 4;
+				curproxy->check_req = malloc(curproxy->check_len+1);
+				snprintf(curproxy->check_req, curproxy->check_len+1, "%s %s %s\r\n", args[2], args[3], vsn);
+
+				if (hdrs) {
+					curproxy->check_hdrs_len = strlen(hdrs) + 2;
+					curproxy->check_hdrs = malloc(curproxy->check_hdrs_len+1);
+					snprintf(curproxy->check_hdrs, curproxy->check_hdrs_len+1, "%s\r\n", hdrs);
+				}
+
+				if (body) {
+					curproxy->check_body_len = strlen(body);
+					curproxy->check_body = strdup(body);
+				}
 			}
 			if (alertif_too_many_args_idx(3, 1, file, linenum, args, &err_code))
 				goto out;
@@ -2953,6 +3001,63 @@
 			if (alertif_too_many_args_idx(0, 1, file, linenum, args, &err_code))
 				goto out;
 		}
+		else if (strcmp(args[1], "send") == 0) {
+			int cur_arg = 2;
+
+			free(curproxy->check_hdrs);
+			free(curproxy->check_body);
+			curproxy->check_hdrs = curproxy->check_body = NULL;
+			curproxy->check_hdrs_len = curproxy->check_body_len = 0;
+			while (*(args[cur_arg])) {
+				if (strcmp(args[cur_arg], "hdr") == 0) {
+					int hdr_len;
+					if (!*(args[cur_arg+1]) || !*(args[cur_arg+2])) {
+						ha_alert("parsing [%s:%d] : '%s %s' : %s expects a name and a value as parameter.\n",
+							 file, linenum, args[0], args[1], args[cur_arg]);
+						err_code |= ERR_ALERT | ERR_FATAL;
+						goto out;
+					}
+
+					cur_arg++;
+					hdr_len = strlen(args[cur_arg]) + strlen(args[cur_arg+1]) + 4;
+					curproxy->check_hdrs = my_realloc2(curproxy->check_hdrs, curproxy->check_hdrs_len+hdr_len+1);
+					if (curproxy->check_hdrs == NULL) {
+						ha_alert("parsing [%s:%d] : out of memory.\n", file, linenum);
+						err_code |= ERR_ALERT | ERR_FATAL;
+						goto out;
+					}
+					snprintf(curproxy->check_hdrs + curproxy->check_hdrs_len, hdr_len+1, "%s: %s\r\n", args[cur_arg], args[cur_arg+1]);
+					curproxy->check_hdrs_len += hdr_len;
+
+					cur_arg++;
+				}
+				else if (strcmp(args[cur_arg], "body") == 0) {
+					if (!*(args[cur_arg+1])) {
+						ha_alert("parsing [%s:%d] : '%s %s' : %s expects a string as parameter.\n",
+							 file, linenum, args[0], args[1], args[cur_arg]);
+						err_code |= ERR_ALERT | ERR_FATAL;
+						goto out;
+					}
+					cur_arg++;
+					free(curproxy->check_body);
+					curproxy->check_body = strdup(args[cur_arg]);
+					curproxy->check_body_len = strlen(args[cur_arg]);
+					if (curproxy->check_body == NULL) {
+						ha_alert("parsing [%s:%d] : out of memory.\n", file, linenum);
+						err_code |= ERR_ALERT | ERR_FATAL;
+						goto out;
+					}
+				}
+				else {
+					ha_alert("parsing [%s:%d] : '%s %s' only supports 'hdr' and 'body', found '%s'.\n",
+						 file, linenum, args[0], args[1], args[cur_arg]);
+					err_code |= ERR_ALERT | ERR_FATAL;
+					goto out;
+				}
+				cur_arg++;
+			}
+
+		}
 		else if (strcmp(args[1], "expect") == 0) {
 			const char *ptr_arg;
 			int cur_arg;
diff --git a/src/checks.c b/src/checks.c
index 0da75c1..13b02bf 100644
--- a/src/checks.c
+++ b/src/checks.c
@@ -1621,13 +1621,30 @@
 			memcpy(b_head(&check->bo) + 11, &gmt_time, 4);
 		}
 		else if ((check->type) == PR_O2_HTTP_CHK) {
-			if (s->proxy->options2 & PR_O2_CHK_SNDST)
-				b_putblk(&check->bo, trash.area,
-					 httpchk_build_status_header(s, trash.area, trash.size));
 			/* prevent HTTP keep-alive when "http-check expect" is used */
 			if (s->proxy->options2 & PR_O2_EXP_TYPE)
 				b_putist(&check->bo, ist("Connection: close\r\n"));
+
+			/* If there is a body, add its content-length */
+			if (s->proxy->check_body_len)
+				chunk_appendf(&check->bo, "Content-Length: %s\r\n", ultoa(s->proxy->check_body_len));
+
+			/* Add configured headers */
+			if (s->proxy->check_hdrs)
+				b_putblk(&check->bo, s->proxy->check_hdrs, s->proxy->check_hdrs_len);
+
+			/* Add send-state header */
+			if (s->proxy->options2 & PR_O2_CHK_SNDST)
+				b_putblk(&check->bo, trash.area,
+					 httpchk_build_status_header(s, trash.area, trash.size));
+
+			/* end-of-header */
 			b_putist(&check->bo, ist("\r\n"));
+
+			/* Add the body */
+			if (s->proxy->check_body)
+				b_putblk(&check->bo, s->proxy->check_body, s->proxy->check_body_len);
+
 			*b_tail(&check->bo) = '\0'; /* to make gdb output easier to read */
 		}
 	}
diff --git a/src/haproxy.c b/src/haproxy.c
index c6e022a..a8d44d8 100644
--- a/src/haproxy.c
+++ b/src/haproxy.c
@@ -2311,6 +2311,8 @@
 		free(p->conf.file);
 		free(p->id);
 		free(p->check_req);
+		free(p->check_hdrs);
+		free(p->check_body);
 		free(p->cookie_name);
 		free(p->cookie_domain);
 		free(p->cookie_attrs);