MINOR: sample: Add fetcher for getting all cookie names

This new fetcher can be used to extract the list of cookie names from
Cookie request header or from Set-Cookie response header depending on
the stream direction. There is an optional argument that can be used
as the delimiter (which is assumed to be the first character of the
argument) between cookie names. The default delimiter is comma (,).

Note that we will treat the Cookie request header as a semi-colon
separated list of cookies and each Set-Cookie response header as
a single cookie and extract the cookie names accordingly.
This commit is contained in:
Ruei-Bang Chen 2023-10-27 13:59:21 -07:00 committed by Willy Tarreau
parent e826bc3dfa
commit 7a1ec235cd
5 changed files with 270 additions and 0 deletions

View File

@ -22265,6 +22265,12 @@ cook_val([<name>]) : integer (deprecated)
returned. If no name is specified, the first cookie value is returned. When
used in ACLs, all matching names are iterated over until a value matches.
req.cook_names([<delim>]) : string
This builds a string made from the concatenation of all cookie names as they
appear in the request (Cookie header) when the rule is evaluated. The default
delimiter is the comma (',') but it may be overridden as an optional argument
<delim>. In this case, only the first character of <delim> is considered.
cookie([<name>]) : string (deprecated)
This extracts the last occurrence of the cookie name <name> on a "Cookie"
header line from the request, or a "Set-Cookie" header from the response, and
@ -22633,6 +22639,15 @@ scook_val([<name>]) : integer (deprecated)
It may be used in tcp-check based expect rules.
res.cook_names([<delim>]) : string
This builds a string made from the concatenation of all cookie names as they
appear in the response (Set-Cookie headers) when the rule is evaluated. The
default delimiter is the comma (',') but it may be overridden as an optional
argument <delim>. In this case, only the first character of <delim> is
considered.
It may be used in tcp-check based expect rules.
res.fhdr([<name>[,<occ>]]) : string
This fetch works like the req.fhdr() fetch with the difference that it acts
on the headers within an HTTP response.

View File

@ -51,6 +51,8 @@ char *http_find_cookie_value_end(char *s, const char *e);
char *http_extract_cookie_value(char *hdr, const char *hdr_end,
char *cookie_name, size_t cookie_name_l,
int list, char **value, size_t *value_l);
char *http_extract_next_cookie_name(char *hdr_beg, char *hdr_end, int is_req,
char **ptr, size_t *len);
int http_parse_qvalue(const char *qvalue, const char **end);
const char *http_find_url_param_pos(const char **chunks,
const char* url_param_name,

View File

@ -2,6 +2,8 @@ varnishtest "cook sample fetch Test"
feature ignore_unknown_macro
# TEST - 1
# Cookie from request
server s1 {
rxreq
txresp
@ -16,9 +18,11 @@ haproxy h1 -conf {
http-request set-var(txn.count) req.cook_cnt()
http-request set-var(txn.val) req.cook_val()
http-request set-var(txn.val_cook2) req.cook_val(cook2)
http-request set-var(txn.cook_names) req.cook_names
http-response set-header count %[var(txn.count)]
http-response set-header val %[var(txn.val)]
http-response set-header val_cook2 %[var(txn.val_cook2)]
http-response set-header cook_names %[var(txn.cook_names)]
default_backend be
@ -34,4 +38,95 @@ client c1 -connect ${h1_fe_sock} {
expect resp.http.count == "3"
expect resp.http.val == "0"
expect resp.http.val_cook2 == "123"
expect resp.http.cook_names == "cook1,cook2,cook3"
} -run
# TEST - 2
# Set-Cookie from response
server s2 {
rxreq
txresp -hdr "Set-Cookie: cook1=0; cook2=123; cook3=22"
} -start
haproxy h2 -conf {
defaults
mode http
frontend fe
bind "fd@${fe}"
http-response set-var(txn.cook_names) res.cook_names
http-response set-header cook_names %[var(txn.cook_names)]
default_backend be
backend be
server srv2 ${s2_addr}:${s2_port}
} -start
client c2 -connect ${h2_fe_sock} {
txreq -url "/"
rxresp
expect resp.status == 200
expect resp.http.cook_names == "cook1"
} -run
# TEST - 3
# Multiple Cookie headers from request
server s3 {
rxreq
txresp
} -start
haproxy h3 -conf {
defaults
mode http
frontend fe
bind "fd@${fe}"
http-request set-var(txn.cook_names) req.cook_names
http-response set-header cook_names %[var(txn.cook_names)]
default_backend be
backend be
server srv3 ${s3_addr}:${s3_port}
} -start
client c3 -connect ${h3_fe_sock} {
txreq -url "/" \
-hdr "cookie: cook1=0; cook2=123; cook3=22" \
-hdr "cookie: cook4=1; cook5=2; cook6=3"
rxresp
expect resp.status == 200
expect resp.http.cook_names == "cook1,cook2,cook3,cook4,cook5,cook6"
} -run
# TEST - 4
# Multiple Set-Cookie headers from response
server s4 {
rxreq
txresp -hdr "Set-Cookie: cook1=0; cook2=123; cook3=22" \
-hdr "Set-Cookie: cook4=1; cook5=2; cook6=3"
} -start
haproxy h4 -conf {
defaults
mode http
frontend fe
bind "fd@${fe}"
http-response set-var(txn.cook_names) res.cook_names
http-response set-header cook_names %[var(txn.cook_names)]
default_backend be
backend be
server srv4 ${s4_addr}:${s4_port}
} -start
client c4 -connect ${h4_fe_sock} {
txreq -url "/"
rxresp
expect resp.status == 200
expect resp.http.cook_names == "cook1,cook4"
} -run

View File

@ -969,6 +969,96 @@ char *http_extract_cookie_value(char *hdr, const char *hdr_end,
return NULL;
}
/* Try to find the next cookie name in a cookie header given a pointer
* <hdr_beg> to the starting position, a pointer <hdr_end> to the ending
* position to search in the cookie and a boolean <is_req> of type int that
* indicates if the stream direction is for request or response.
* The lookup begins at <hdr_beg>, which is assumed to be in
* Cookie / Set-Cookie header, and the function returns a pointer to the next
* position to search from if a valid cookie k-v pair is found for Cookie
* request header (<is_req> is non-zero) and <hdr_end> for Set-Cookie response
* header (<is_req> is zero). When the next cookie name is found, <ptr> will
* be pointing to the start of the cookie name, and <len> will be the length
* of the cookie name.
* Otherwise if there is no valid cookie k-v pair, NULL is returned.
* The <hdr_end> pointer must point to the first character
* not part of the Cookie / Set-Cookie header.
*/
char *http_extract_next_cookie_name(char *hdr_beg, char *hdr_end, int is_req,
char **ptr, size_t *len)
{
char *equal, *att_end, *att_beg, *val_beg;
char *next;
/* We search a valid cookie name between hdr_beg and hdr_end,
* followed by an equal. For example for the following cookie:
* Cookie: NAME1 = VALUE 1 ; NAME2 = VALUE2 ; NAME3 = VALUE3\r\n
* We want to find NAME1, NAME2, or NAME3 depending on where we start our search
* according to <hdr_beg>
*/
for (att_beg = hdr_beg; att_beg + 1 < hdr_end; att_beg = next + 1) {
while (att_beg < hdr_end && HTTP_IS_SPHT(*att_beg))
att_beg++;
/* find <att_end> : this is the first character after the last non
* space before the equal. It may be equal to <hdr_end>.
*/
equal = att_end = att_beg;
while (equal < hdr_end) {
if (*equal == '=' || *equal == ';')
break;
if (HTTP_IS_SPHT(*equal++))
continue;
att_end = equal;
}
/* Here, <equal> points to '=', a delimiter or the end. <att_end>
* is between <att_beg> and <equal>, both may be identical.
*/
/* Look for end of cookie if there is an equal sign */
if (equal < hdr_end && *equal == '=') {
/* Look for the beginning of the value */
val_beg = equal + 1;
while (val_beg < hdr_end && HTTP_IS_SPHT(*val_beg))
val_beg++;
/* Find the end of the value, respecting quotes */
next = http_find_cookie_value_end(val_beg, hdr_end);
} else {
next = equal;
}
/* We have nothing to do with attributes beginning with '$'. However,
* they will automatically be removed if a header before them is removed,
* since they're supposed to be linked together.
*/
if (*att_beg == '$')
continue;
/* Ignore cookies with no equal sign */
if (equal == next)
continue;
/* Now we have the cookie name between <att_beg> and <att_end>, and
* <next> points to the end of cookie value
*/
*ptr = att_beg;
*len = att_end - att_beg;
/* Return next position for Cookie request header and <hdr_end> for
* Set-Cookie response header as each Set-Cookie header is assumed to
* contain only 1 cookie
*/
if (is_req)
return next + 1;
return hdr_end;
}
return NULL;
}
/* Parses a qvalue and returns it multiplied by 1000, from 0 to 1000. If the
* value is larger than 1000, it is bound to 1000. The parser consumes up to
* 1 digit, one dot and 3 digits and stops on the first invalid character.

View File

@ -1824,6 +1824,72 @@ static int smp_fetch_cookie_val(const struct arg *args, struct sample *smp, cons
return ret;
}
/* Iterate over all cookies present in a message,
* and return the list of cookie names separated by
* the input argument character.
* If no input argument is provided,
* the default delimiter is ','.
* The returned sample is of type CSTR.
*/
static int smp_fetch_cookie_names(const struct arg *args, struct sample *smp, const char *kw, void *private)
{
/* possible keywords: req.cook_names, res.cook_names */
struct channel *chn = ((kw[2] == 'q') ? SMP_REQ_CHN(smp) : SMP_RES_CHN(smp));
struct check *check = ((kw[2] == 's') ? objt_check(smp->sess->origin) : NULL);
struct htx *htx = smp_prefetch_htx(smp, chn, check, 1);
struct http_hdr_ctx ctx;
struct ist hdr;
struct buffer *temp;
char del = ',';
char *ptr, *attr_beg, *attr_end;
size_t len = 0;
int is_req = !(check || (chn && chn->flags & CF_ISRESP));
if (!htx)
return 0;
if (args->type == ARGT_STR)
del = *args[0].data.str.area;
hdr = (is_req ? ist("Cookie") : ist("Set-Cookie"));
temp = get_trash_chunk();
smp->flags |= SMP_F_VOL_HDR;
attr_end = attr_beg = NULL;
ctx.blk = NULL;
/* Scan through all headers and extract all cookie names from
* 1. Cookie header(s) for request channel OR
* 2. Set-Cookie header(s) for response channel
*/
while (1) {
/* Note: attr_beg == NULL every time we need to fetch a new header */
if (!attr_beg) {
/* For Set-Cookie, we need to fetch the entire header line (set flag to 1) */
if (!http_find_header(htx, hdr, &ctx, !is_req))
break;
attr_beg = ctx.value.ptr;
attr_end = attr_beg + ctx.value.len;
}
while (1) {
attr_beg = http_extract_next_cookie_name(attr_beg, attr_end, is_req, &ptr, &len);
if (!attr_beg)
break;
/* prepend delimiter if this is not the first cookie name found */
if (temp->data)
temp->area[temp->data++] = del;
/* At this point ptr should point to the start of the cookie name and len would be the length of the cookie name */
if (!chunk_memcat(temp, ptr, len))
return 0;
}
}
smp->data.type = SMP_T_STR;
smp->data.u.str = *temp;
return 1;
}
/************************************************************************/
/* The code below is dedicated to sample fetches */
/************************************************************************/
@ -2208,6 +2274,7 @@ static struct sample_fetch_kw_list sample_fetch_keywords = {ILH, {
{ "req.cook", smp_fetch_cookie, ARG1(0,STR), NULL, SMP_T_STR, SMP_USE_HRQHV },
{ "req.cook_cnt", smp_fetch_cookie_cnt, ARG1(0,STR), NULL, SMP_T_SINT, SMP_USE_HRQHV },
{ "req.cook_val", smp_fetch_cookie_val, ARG1(0,STR), NULL, SMP_T_SINT, SMP_USE_HRQHV },
{ "req.cook_names", smp_fetch_cookie_names, ARG1(0,STR), NULL, SMP_T_STR, SMP_USE_HRQHV },
{ "req.fhdr", smp_fetch_fhdr, ARG2(0,STR,SINT), val_hdr, SMP_T_STR, SMP_USE_HRQHV },
{ "req.fhdr_cnt", smp_fetch_fhdr_cnt, ARG1(0,STR), NULL, SMP_T_SINT, SMP_USE_HRQHV },
@ -2221,6 +2288,7 @@ static struct sample_fetch_kw_list sample_fetch_keywords = {ILH, {
{ "res.cook", smp_fetch_cookie, ARG1(0,STR), NULL, SMP_T_STR, SMP_USE_HRSHV },
{ "res.cook_cnt", smp_fetch_cookie_cnt, ARG1(0,STR), NULL, SMP_T_SINT, SMP_USE_HRSHV },
{ "res.cook_val", smp_fetch_cookie_val, ARG1(0,STR), NULL, SMP_T_SINT, SMP_USE_HRSHV },
{ "res.cook_names", smp_fetch_cookie_names, ARG1(0,STR), NULL, SMP_T_STR, SMP_USE_HRSHV },
{ "res.fhdr", smp_fetch_fhdr, ARG2(0,STR,SINT), val_hdr, SMP_T_STR, SMP_USE_HRSHV },
{ "res.fhdr_cnt", smp_fetch_fhdr_cnt, ARG1(0,STR), NULL, SMP_T_SINT, SMP_USE_HRSHV },