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 },