From 7a1ec235cd69628a6bcece061e85fe3bfc9d488a Mon Sep 17 00:00:00 2001 From: Ruei-Bang Chen Date: Fri, 27 Oct 2023 13:59:21 -0700 Subject: [PATCH] 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. --- doc/configuration.txt | 15 +++++ include/haproxy/http.h | 2 + reg-tests/sample_fetches/cook.vtc | 95 +++++++++++++++++++++++++++++++ src/http.c | 90 +++++++++++++++++++++++++++++ src/http_fetch.c | 68 ++++++++++++++++++++++ 5 files changed, 270 insertions(+) diff --git a/doc/configuration.txt b/doc/configuration.txt index 3dcdb3e2a..20e000eb5 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -22265,6 +22265,12 @@ cook_val([]) : 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([]) : 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 + . In this case, only the first character of is considered. + cookie([]) : string (deprecated) This extracts the last occurrence of the cookie name on a "Cookie" header line from the request, or a "Set-Cookie" header from the response, and @@ -22633,6 +22639,15 @@ scook_val([]) : integer (deprecated) It may be used in tcp-check based expect rules. +res.cook_names([]) : 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 . In this case, only the first character of is + considered. + + It may be used in tcp-check based expect rules. + res.fhdr([[,]]) : string This fetch works like the req.fhdr() fetch with the difference that it acts on the headers within an HTTP response. diff --git a/include/haproxy/http.h b/include/haproxy/http.h index e2ba2e515..299264051 100644 --- a/include/haproxy/http.h +++ b/include/haproxy/http.h @@ -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, diff --git a/reg-tests/sample_fetches/cook.vtc b/reg-tests/sample_fetches/cook.vtc index e2c1284da..b0f547215 100644 --- a/reg-tests/sample_fetches/cook.vtc +++ b/reg-tests/sample_fetches/cook.vtc @@ -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 diff --git a/src/http.c b/src/http.c index 2436292b2..9599e0eb5 100644 --- a/src/http.c +++ b/src/http.c @@ -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 + * to the starting position, a pointer to the ending + * position to search in the cookie and a boolean of type int that + * indicates if the stream direction is for request or response. + * The lookup begins at , 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 non-zero) and for Set-Cookie response + * header ( is zero). When the next cookie name is found, will + * be pointing to the start of the cookie name, and will be the length + * of the cookie name. + * Otherwise if there is no valid cookie k-v pair, NULL is returned. + * The 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 + */ + 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 : this is the first character after the last non + * space before the equal. It may be equal to . + */ + equal = att_end = att_beg; + + while (equal < hdr_end) { + if (*equal == '=' || *equal == ';') + break; + if (HTTP_IS_SPHT(*equal++)) + continue; + att_end = equal; + } + + /* Here, points to '=', a delimiter or the end. + * is between and , 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 and , and + * points to the end of cookie value + */ + *ptr = att_beg; + *len = att_end - att_beg; + + /* Return next position for Cookie request header and 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. diff --git a/src/http_fetch.c b/src/http_fetch.c index ff2365e47..f500ad727 100644 --- a/src/http_fetch.c +++ b/src/http_fetch.c @@ -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 },