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.
This commit is contained in:
Christopher Faulet 2020-04-09 08:44:06 +02:00
parent bc1f54b0fc
commit 8acb1284bc
6 changed files with 324 additions and 14 deletions

View File

@ -4491,6 +4491,33 @@ http-check expect [!] <match> <pattern>
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
@ -7143,8 +7170,7 @@ option httpchk <method> <uri> <version>
<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
@ -7158,12 +7184,18 @@ option httpchk <method> <uri> <version>
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",

View File

@ -426,6 +426,10 @@ struct proxy {
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 */

View File

@ -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

View File

@ -291,6 +291,18 @@ int cfg_parse_listen(const char *file, int linenum, char **args, int kwm)
}
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) {
@ -2309,7 +2321,10 @@ stats_error_parsing:
/* 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 */
@ -2320,16 +2335,49 @@ stats_error_parsing:
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;
@ -2811,6 +2859,63 @@ stats_error_parsing:
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;

View File

@ -1599,13 +1599,30 @@ static int connect_conn_chk(struct task *t)
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 */
}
}

View File

@ -2527,6 +2527,8 @@ void deinit(void)
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);