[MEDIUM] http: fix space handling in the request cookie parser

The request cookie parser did not allow spaces to appear in cookie
values nor around the equal sign. The various RFCs on the subject
say different things, some suggesting that a space is allowed after
the equal sign and being worded in a way that lets one believe it
is allowed before too. Some spaces may appear inside values and be
part of the values. The quotes allow delimiters to be embedded in
values. The spaces before and after attributes should be trimmed.

The new parser addresses all those points and has been carefully tested.
It fixes misplaced spaces around equal signs before processing the cookies
or forwarding them. It also tries its best to perform clean removals by
always keeping the delimiter after the value being removed and leaving one
space after it.

The variable inside the parser have been renamed to make the code a lot
more understandable, and one multi-function pointer has been eliminated.

Since this patch fixes real possible issues, it should be backported to 1.4
and possibly 1.3, since one (single) case of wrong spaces has been reported
in 1.3.

The code handling the Set-Cookie has not been touched yet.
diff --git a/src/proto_http.c b/src/proto_http.c
index f8b46f6..487ed69 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -5625,46 +5625,122 @@
 	}
 }
 
+/* Find the end of a cookie value contained between <s> and <e>. It works the
+ * same way as with headers above except that the semi-colon also ends a token.
+ * See RFC2965 for more information. Note that it requires a valid header to
+ * return a valid result.
+ */
+char *find_cookie_value_end(char *s, const char *e)
+{
+	int quoted, qdpair;
+
+	quoted = qdpair = 0;
+	for (; s < e; s++) {
+		if (qdpair)                    qdpair = 0;
+		else if (quoted) {
+			if (*s == '\\')        qdpair = 1;
+			else if (*s == '"')    quoted = 0;
+		}
+		else if (*s == '"')            quoted = 1;
+		else if (*s == ',' || *s == ';') return s;
+	}
+	return s;
+}
+
+/* Delete a value in a header between delimiters <from> and <next> in buffer
+ * <buf>. The number of characters displaced is returned, and the pointer to
+ * the first delimiter is updated if required. The function tries as much as
+ * possible to respect the following principles :
+ *  - replace <from> delimiter by the <next> one unless <from> points to a
+ *    colon, in which case <next> is simply removed
+ *  - set exactly one space character after the new first delimiter, unless
+ *    there are not enough characters in the block being moved to do so.
+ *  - remove unneeded spaces before the previous delimiter and after the new
+ *    one.
+ *
+ * It is the caller's responsibility to ensure that :
+ *   - <from> points to a valid delimiter or the colon ;
+ *   - <next> points to a valid delimiter or the final CR/LF ;
+ *   - there are non-space chars before <from> ;
+ *   - there is a CR/LF at or after <next>.
+ */
+int del_hdr_value(struct buffer *buf, char **from, char *next)
+{
+	char *prev = *from;
+
+	if (*prev == ':') {
+		/* We're removing the first value, preserve the colon and add a
+		 * space if possible.
+		 */
+		if (!http_is_crlf[(unsigned char)*next])
+			next++;
+		prev++;
+		if (prev < next)
+			*prev++ = ' ';
+
+		while (http_is_spht[(unsigned char)*next])
+			next++;
+	} else {
+		/* Remove useless spaces before the old delimiter. */
+		while (http_is_spht[(unsigned char)*(prev-1)])
+			prev--;
+		*from = prev;
+
+		/* copy the delimiter and if possible a space if we're
+		 * not at the end of the line.
+		 */
+		if (!http_is_crlf[(unsigned char)*next]) {
+			*prev++ = *next++;
+			if (prev + 1 < next)
+				*prev++ = ' ';
+			while (http_is_spht[(unsigned char)*next])
+				next++;
+		}
+	}
+	return buffer_replace2(buf, prev, next, NULL, 0);
+}
+
 /*
  * Manage client-side cookie. It can impact performance by about 2% so it is
- * desirable to call it only when needed.
+ * desirable to call it only when needed. This code is quite complex because
+ * of the multiple very crappy and ambiguous syntaxes we have to support. it
+ * highly recommended not to touch this part without a good reason !
  */
 void manage_client_side_cookies(struct session *t, struct buffer *req)
 {
 	struct http_txn *txn = &t->txn;
-	char *p1, *p2, *p3, *p4, *p5;
-	char *del_colon, *del_cookie, *colon;
-	int app_cookies;
-
-	char *cur_ptr, *cur_end, *cur_next;
+	int preserve_hdr;
 	int cur_idx, old_idx;
+	char *hdr_beg, *hdr_end, *hdr_next, *del_from;
+	char *prev, *att_beg, *att_end, *equal, *val_beg, *val_end, *next;
 
-	/* Iterate through the headers.
-	 * we start with the start line.
-	 */
+	/* Iterate through the headers, we start with the start line. */
 	old_idx = 0;
-	cur_next = txn->req.sol + hdr_idx_first_pos(&txn->hdr_idx);
+	hdr_next = txn->req.sol + hdr_idx_first_pos(&txn->hdr_idx);
 
 	while ((cur_idx = txn->hdr_idx.v[old_idx].next)) {
 		struct hdr_idx_elem *cur_hdr;
 		int val;
 
 		cur_hdr  = &txn->hdr_idx.v[cur_idx];
-		cur_ptr  = cur_next;
-		cur_end  = cur_ptr + cur_hdr->len;
-		cur_next = cur_end + cur_hdr->cr + 1;
+		hdr_beg  = hdr_next;
+		hdr_end  = hdr_beg + cur_hdr->len;
+		hdr_next = hdr_end + cur_hdr->cr + 1;
 
-		/* We have one full header between cur_ptr and cur_end, and the
-		 * next header starts at cur_next. We're only interested in
+		/* We have one full header between hdr_beg and hdr_end, and the
+		 * next header starts at hdr_next. We're only interested in
 		 * "Cookie:" headers.
 		 */
 
-		val = http_header_match2(cur_ptr, cur_end, "Cookie", 6);
+		val = http_header_match2(hdr_beg, hdr_end, "Cookie", 6);
 		if (!val) {
 			old_idx = cur_idx;
 			continue;
 		}
 
+		del_from = NULL;  /* nothing to be deleted */
+		preserve_hdr = 0; /* assume we may kill the whole header */
+
 		/* Now look for cookies. Conforming to RFC2109, we have to support
 		 * attributes whose name begin with a '$', and associate them with
 		 * the right cookie, if we want to delete this cookie.
@@ -5676,267 +5752,336 @@
 		 *    "special" cookie.
 		 * At the end of loop, if a "special" cookie remains, we may have to
 		 * remove it. If no application cookie persists in the header, we
-		 * *MUST* delete it
+		 * *MUST* delete it.
+		 *
+		 * Note: RFC2965 is unclear about the processing of spaces around
+		 * the equal sign in the ATTR=VALUE form. A careful inspection of
+		 * the RFC explicitly allows spaces before it, and not within the
+		 * tokens (attrs or values). An inspection of RFC2109 allows that
+		 * too but section 10.1.3 lets one think that spaces may be allowed
+		 * after the equal sign too, resulting in some (rare) buggy
+		 * implementations trying to do that. So let's do what servers do.
+		 * Latest ietf draft forbids spaces all around. Also, earlier RFCs
+		 * allowed quoted strings in values, with any possible character
+		 * after a backslash, including control chars and delimitors, which
+		 * causes parsing to become ambiguous. Browsers also allow spaces
+		 * within values even without quotes.
+		 *
+		 * We have to keep multiple pointers in order to support cookie
+		 * removal at the beginning, middle or end of header without
+		 * corrupting the header. All of these headers are valid :
+		 *
+		 * Cookie:NAME1=VALUE1;NAME2=VALUE2;NAME3=VALUE3\r\n
+		 * Cookie:NAME1=VALUE1;NAME2_ONLY ;NAME3=VALUE3\r\n
+		 * Cookie:    NAME1  =  VALUE 1  ; NAME2 = VALUE2 ; NAME3 = VALUE3\r\n
+		 * |     |    |    | |  |      | |                                |
+		 * |     |    |    | |  |      | |                     hdr_end <--+
+		 * |     |    |    | |  |      | +--> next
+		 * |     |    |    | |  |      +----> val_end
+		 * |     |    |    | |  +-----------> val_beg
+		 * |     |    |    | +--------------> equal
+		 * |     |    |    +----------------> att_end
+		 * |     |    +---------------------> att_beg
+		 * |     +--------------------------> prev
+		 * +--------------------------------> hdr_beg
 		 */
 
+		for (prev = hdr_beg + 6; prev < hdr_end; prev = next) {
+			/* Iterate through all cookies on this line */
+
-		colon = p1 = cur_ptr + val; /* first non-space char after 'Cookie:' */
+			/* find att_beg */
+			att_beg = prev + 1;
+			while (att_beg < hdr_end && http_is_spht[(unsigned char)*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;
 
-		/* del_cookie == NULL => nothing to be deleted */
-		del_colon = del_cookie = NULL;
-		app_cookies = 0;
-		
-		while (p1 < cur_end) {
-			/* skip spaces and colons, but keep an eye on these ones */
-		resync_name:
-			while (p1 < cur_end) {
-				if (*p1 == ';' || *p1 == ',')
-					colon = p1;
-				else if (!isspace((unsigned char)*p1))
+			while (equal < hdr_end) {
+				if (*equal == '=' || *equal == ',' || *equal == ';')
 					break;
-				p1++;
+				if (http_is_spht[(unsigned char)*equal++])
+					continue;
+				att_end = equal;
 			}
 
-			if (p1 == cur_end)
-				break;
-		    
-			/* p1 is at the beginning of the cookie name */
-			p2 = p1;
-			while (p2 < cur_end && *p2 != '=') {
-				if (*p2 == ',' || *p2 == ';' || isspace((unsigned char)*p2)) {
-					/* oops, the cookie name was truncated, resync */
-					p1 = p2;
-					goto resync_name;
-				}
-				p2++;
+			/* here, <equal> points to '=', a delimitor 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[(unsigned char)*val_beg])
+					val_beg++;
+
+				/* find the end of the value, respecting quotes */
+				next = find_cookie_value_end(val_beg, hdr_end);
+
+				/* make val_end point to the first white space or delimitor after the value */
+				val_end = next;
+				while (val_end > val_beg && http_is_spht[(unsigned char)*(val_end - 1)])
+					val_end--;
+			} else {
+				val_beg = val_end = next = equal;
 			}
 
-			if (p2 == cur_end)
-				break;
+			/* 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;
 
-			p3 = p2 + 1; /* skips the '=' sign */
-			if (p3 == cur_end)
-				break;
-		    
-			/* parse the value, stripping leading and trailing spaces but keeping insiders. */
-			p5 = p4 = p3;
-			while (p5 < cur_end && *p5 != ';' && *p5 != ',') {
-				if (!isspace((unsigned char)*p5))
-					p4 = p5 + 1;
-				p5++;
+			/* Ignore cookies with no equal sign */
+			if (equal == next) {
+				/* This is not our cookie, so we must preserve it. But if we already
+				 * scheduled another cookie for removal, we cannot remove the
+				 * complete header, but we can remove the previous block itself.
+				 */
+				preserve_hdr = 1;
+				if (del_from != NULL) {
+					int delta = del_hdr_value(req, &del_from, prev);
+					val_end  += delta;
+					next     += delta;
+					hdr_end  += delta;
+					hdr_next += delta;
+					cur_hdr->len += delta;
+					http_msg_move_end(&txn->req, delta);
+					prev     = del_from;
+					del_from = NULL;
+				}
+				continue;
 			}
 
-			/* here, we have the cookie name between p1 and p2,
-			 * and its value between p3 and p4.
-			 * we can process it :
-			 *
-			 * Cookie: NAME=VALUE ;
-			 * |      ||   ||    |+-> p5
-			 * |      ||   ||    +--> p4
-			 * |      ||   |+-------> p3
-			 * |      ||   +--------> p2
-			 * |      |+------------> p1
-			 * |      +-------------> colon
-			 * +--------------------> cur_ptr
+			/* if there are spaces around the equal sign, we need to
+			 * strip them otherwise we'll get trouble for cookie captures,
+			 * or even for rewrites. Since this happens extremely rarely,
+			 * it does not hurt performance.
 			 */
-		    
-			if (*p1 == '$') {
-				/* skip this one */
+			if (unlikely(att_end != equal || val_beg > equal + 1)) {
+				int stripped_before = 0;
+				int stripped_after = 0;
+
+				if (att_end != equal) {
+					stripped_before = buffer_replace2(req, att_end, equal, NULL, 0);
+					equal   += stripped_before;
+					val_beg += stripped_before;
+				}
+
+				if (val_beg > equal + 1) {
+					stripped_after = buffer_replace2(req, equal + 1, val_beg, NULL, 0);
+					val_beg += stripped_after;
+					stripped_before += stripped_after;
+				}
+
+				val_end      += stripped_before;
+				next         += stripped_before;
+				hdr_end      += stripped_before;
+				hdr_next     += stripped_before;
+				cur_hdr->len += stripped_before;
+				http_msg_move_end(&txn->req, stripped_before);
 			}
-			else {
-				/* first, let's see if we want to capture it */
-				if (t->fe->capture_name != NULL &&
-				    txn->cli_cookie == NULL &&
-				    (p4 - p1 >= t->fe->capture_namelen) &&
-				    memcmp(p1, t->fe->capture_name, t->fe->capture_namelen) == 0) {
-					int log_len = p4 - p1;
+			/* now everything is as on the diagram above */
 
-					if ((txn->cli_cookie = pool_alloc2(pool2_capture)) == NULL) {
-						Alert("HTTP logging : out of memory.\n");
-					} else {
-						if (log_len > t->fe->capture_len)
-							log_len = t->fe->capture_len;
-						memcpy(txn->cli_cookie, p1, log_len);
-						txn->cli_cookie[log_len] = 0;
-					}
+			/* First, let's see if we want to capture this cookie. We check
+			 * that we don't already have a client side cookie, because we
+			 * can only capture one. Also as an optimisation, we ignore
+			 * cookies shorter than the declared name.
+			 */
+			if (t->fe->capture_name != NULL && txn->cli_cookie == NULL &&
+			    (val_end - att_beg >= t->fe->capture_namelen) &&
+			    memcmp(att_beg, t->fe->capture_name, t->fe->capture_namelen) == 0) {
+				int log_len = val_end - att_beg;
+
+				if ((txn->cli_cookie = pool_alloc2(pool2_capture)) == NULL) {
+					Alert("HTTP logging : out of memory.\n");
+				} else {
+					if (log_len > t->fe->capture_len)
+						log_len = t->fe->capture_len;
+					memcpy(txn->cli_cookie, att_beg, log_len);
+					txn->cli_cookie[log_len] = 0;
 				}
+			}
 
-				if ((p2 - p1 == t->be->cookie_len) && (t->be->cookie_name != NULL) &&
-				    (memcmp(p1, t->be->cookie_name, p2 - p1) == 0)) {
-					/* Cool... it's the right one */
-					struct server *srv = t->be->srv;
-					char *delim;
+			/* For cookies in prefix mode. The form is :
+			 *
+			 *    Cookie: NAME=SRV~VALUE
+			 *
+			 */
+			if ((att_end - att_beg == t->be->cookie_len) && (t->be->cookie_name != NULL) &&
+			    (memcmp(att_beg, t->be->cookie_name, att_end - att_beg) == 0)) {
+				struct server *srv = t->be->srv;
+				char *delim;
 
-					/* if we're in cookie prefix mode, we'll search the delimitor so that we
-					 * have the server ID betweek p3 and delim, and the original cookie between
-					 * delim+1 and p4. Otherwise, delim==p4 :
-					 *
-					 * Cookie: NAME=SRV~VALUE ;
-					 * |      ||   ||  |     |+-> p5
-					 * |      ||   ||  |     +--> p4
-					 * |      ||   ||  +--------> delim
-					 * |      ||   |+-----------> p3
-					 * |      ||   +------------> p2
-					 * |      |+----------------> p1
-					 * |      +-----------------> colon
-					 * +------------------------> cur_ptr
-					 */
+				/* if we're in cookie prefix mode, we'll search the delimitor so that we
+				 * have the server ID between val_beg and delim, and the original cookie between
+				 * delim+1 and val_end. Otherwise, delim==val_end :
+				 *
+				 * Cookie: NAME=SRV;          # in all but prefix modes
+				 * Cookie: NAME=SRV~OPAQUE ;  # in prefix mode
+				 * |      ||   ||  |      |+-> next
+				 * |      ||   ||  |      +--> val_end
+				 * |      ||   ||  +---------> delim
+				 * |      ||   |+------------> val_beg
+				 * |      ||   +-------------> att_end = equal
+				 * |      |+-----------------> att_beg
+				 * |      +------------------> prev
+				 * +-------------------------> hdr_beg
+				 */
 
-					if (t->be->options & PR_O_COOK_PFX) {
-						for (delim = p3; delim < p4; delim++)
-							if (*delim == COOKIE_DELIM)
-								break;
-					}
-					else
-						delim = p4;
+				if (t->be->options & PR_O_COOK_PFX) {
+					for (delim = val_beg; delim < val_end; delim++)
+						if (*delim == COOKIE_DELIM)
+							break;
+				}
+				else
+					delim = val_end;
 
 
-					/* Here, we'll look for the first running server which supports the cookie.
-					 * This allows to share a same cookie between several servers, for example
-					 * to dedicate backup servers to specific servers only.
-					 * However, to prevent clients from sticking to cookie-less backup server
-					 * when they have incidentely learned an empty cookie, we simply ignore
-					 * empty cookies and mark them as invalid.
-					 * The same behaviour is applied when persistence must be ignored.
-					 */
-					if ((delim == p3) || (t->flags & SN_IGNORE_PRST))
-						srv = NULL;
+				/* Here, we'll look for the first running server which supports the cookie.
+				 * This allows to share a same cookie between several servers, for example
+				 * to dedicate backup servers to specific servers only.
+				 * However, to prevent clients from sticking to cookie-less backup server
+				 * when they have incidentely learned an empty cookie, we simply ignore
+				 * empty cookies and mark them as invalid.
+				 * The same behaviour is applied when persistence must be ignored.
+				 */
+				if ((delim == val_beg) || (t->flags & SN_IGNORE_PRST))
+					srv = NULL;
 
-					while (srv) {
-						if (srv->cookie && (srv->cklen == delim - p3) &&
-						    !memcmp(p3, srv->cookie, delim - p3)) {
-							if ((srv->state & SRV_RUNNING) ||
-							    (t->be->options & PR_O_PERSIST) ||
-							    (t->flags & SN_FORCE_PRST)) {
-								/* we found the server and it's usable */
-								txn->flags &= ~TX_CK_MASK;
-								txn->flags |= (srv->state & SRV_RUNNING) ? TX_CK_VALID : TX_CK_DOWN;
-								t->flags |= SN_DIRECT | SN_ASSIGNED;
-								t->srv = srv;
-								break;
-							} else {
-								/* we found a server, but it's down */
-								txn->flags &= ~TX_CK_MASK;
-								txn->flags |= TX_CK_DOWN;
-							}
+				while (srv) {
+					if (srv->cookie && (srv->cklen == delim - val_beg) &&
+					    !memcmp(val_beg, srv->cookie, delim - val_beg)) {
+						if ((srv->state & SRV_RUNNING) ||
+						    (t->be->options & PR_O_PERSIST) ||
+						    (t->flags & SN_FORCE_PRST)) {
+							/* we found the server and we can use it */
+							txn->flags &= ~TX_CK_MASK;
+							txn->flags |= (srv->state & SRV_RUNNING) ? TX_CK_VALID : TX_CK_DOWN;
+							t->flags |= SN_DIRECT | SN_ASSIGNED;
+							t->srv = srv;
+							break;
+						} else {
+							/* we found a server, but it's down,
+							 * mark it as such and go on in case
+							 * another one is available.
+							 */
+							txn->flags &= ~TX_CK_MASK;
+							txn->flags |= TX_CK_DOWN;
 						}
-						srv = srv->next;
-					}
-
-					if (!srv && !(txn->flags & TX_CK_DOWN)) {
-						/* no server matched this cookie */
-						txn->flags &= ~TX_CK_MASK;
-						txn->flags |= TX_CK_INVALID;
 					}
+					srv = srv->next;
+				}
 
-					/* depending on the cookie mode, we may have to either :
-					 * - delete the complete cookie if we're in insert+indirect mode, so that
-					 *   the server never sees it ;
-					 * - remove the server id from the cookie value, and tag the cookie as an
-					 *   application cookie so that it does not get accidentely removed later,
-					 *   if we're in cookie prefix mode
-					 */
-					if ((t->be->options & PR_O_COOK_PFX) && (delim != p4)) {
-						int delta; /* negative */
+				if (!srv && !(txn->flags & TX_CK_DOWN)) {
+					/* no server matched this cookie */
+					txn->flags &= ~TX_CK_MASK;
+					txn->flags |= TX_CK_INVALID;
+				}
 
-						delta = buffer_replace2(req, p3, delim + 1, NULL, 0);
-						p4  += delta;
-						p5  += delta;
-						cur_end += delta;
-						cur_next += delta;
-						cur_hdr->len += delta;
-						http_msg_move_end(&txn->req, delta);
+				/* depending on the cookie mode, we may have to either :
+				 * - delete the complete cookie if we're in insert+indirect mode, so that
+				 *   the server never sees it ;
+				 * - remove the server id from the cookie value, and tag the cookie as an
+				 *   application cookie so that it does not get accidentely removed later,
+				 *   if we're in cookie prefix mode
+				 */
+				if ((t->be->options & PR_O_COOK_PFX) && (delim != val_end)) {
+					int delta; /* negative */
 
-						del_cookie = del_colon = NULL;
-						app_cookies++;	/* protect the header from deletion */
-					}
-					else if (del_cookie == NULL &&
-						 (t->be->options & (PR_O_COOK_INS | PR_O_COOK_IND)) == (PR_O_COOK_INS | PR_O_COOK_IND)) {
-						del_cookie = p1;
-						del_colon = colon;
-					}
-				} else {
-					/* now we know that we must keep this cookie since it's
-					 * not ours. But if we wanted to delete our cookie
-					 * earlier, we cannot remove the complete header, but we
-					 * can remove the previous block itself.
-					 */
-					app_cookies++;
+					delta = buffer_replace2(req, val_beg, delim + 1, NULL, 0);
+					val_end  += delta;
+					next     += delta;
+					hdr_end  += delta;
+					hdr_next += delta;
+					cur_hdr->len += delta;
+					http_msg_move_end(&txn->req, delta);
 
-					if (del_cookie != NULL) {
-						int delta; /* negative */
+					del_from = NULL;
+					preserve_hdr = 1; /* we want to keep this cookie */
+				}
+				else if (del_from == NULL &&
+					 (t->be->options & (PR_O_COOK_INS | PR_O_COOK_IND)) == (PR_O_COOK_INS | PR_O_COOK_IND)) {
+					del_from = prev;
+				}
+			} else {
+				/* This is not our cookie, so we must preserve it. But if we already
+				 * scheduled another cookie for removal, we cannot remove the
+				 * complete header, but we can remove the previous block itself.
+				 */
+				preserve_hdr = 1;
 
-						delta = buffer_replace2(req, del_cookie, p1, NULL, 0);
-						p4  += delta;
-						p5  += delta;
-						cur_end += delta;
-						cur_next += delta;
-						cur_hdr->len += delta;
-						http_msg_move_end(&txn->req, delta);
-						del_cookie = del_colon = NULL;
-					}
+				if (del_from != NULL) {
+					int delta = del_hdr_value(req, &del_from, prev);
+					val_end  += delta;
+					next     += delta;
+					hdr_end  += delta;
+					hdr_next += delta;
+					cur_hdr->len += delta;
+					http_msg_move_end(&txn->req, delta);
+					prev     = del_from;
+					del_from = NULL;
 				}
+			}
 
-				/* Look for the appsession cookie unless persistence must be ignored */
-				if (!(t->flags & SN_IGNORE_PRST) && (t->be->appsession_name != NULL)) {
-					int cmp_len, value_len;
-					char *value_begin;
+			/* Look for the appsession cookie unless persistence must be ignored */
+			if (!(t->flags & SN_IGNORE_PRST) && (t->be->appsession_name != NULL)) {
+				int cmp_len, value_len;
+				char *value_begin;
 
-					if (t->be->options2 & PR_O2_AS_PFX) {
-						cmp_len = MIN(p4 - p1, t->be->appsession_name_len);
-						value_begin = p1 + t->be->appsession_name_len;
-						value_len = p4 - p1 - t->be->appsession_name_len;
-					} else {
-						cmp_len = p2 - p1;
-						value_begin = p3;
-						value_len = p4 - p3;
-					}
+				if (t->be->options2 & PR_O2_AS_PFX) {
+					cmp_len     = MIN(val_end - att_beg, t->be->appsession_name_len);
+					value_begin = att_beg + t->be->appsession_name_len;
+					value_len   = val_end - att_beg - t->be->appsession_name_len;
+				} else {
+					cmp_len     = att_end - att_beg;
+					value_begin = val_beg;
+					value_len   = val_end - val_beg;
+				}
 
-					/* let's see if the cookie is our appcookie */
-					if ((cmp_len == t->be->appsession_name_len) &&
-					    (memcmp(p1, t->be->appsession_name, t->be->appsession_name_len) == 0)) {
-						/* Cool... it's the right one */
-						manage_client_side_appsession(t, value_begin, value_len);
-					}
-#if defined(DEBUG_HASH)
-					Alert("manage_client_side_cookies\n");
-					appsession_hash_dump(&(t->be->htbl_proxy));
-#endif
-				}/* end if ((t->proxy->appsession_name != NULL) ... */
+				/* let's see if the cookie is our appcookie */
+				if (cmp_len == t->be->appsession_name_len &&
+				    memcmp(att_beg, t->be->appsession_name, cmp_len) == 0) {
+					manage_client_side_appsession(t, value_begin, value_len);
+				}
 			}
 
-			/* we'll have to look for another cookie ... */
-			p1 = p5;
-		} /* while (p1 < cur_end) */
+			/* continue with next cookie on this header line */
+			att_beg = next;
+		} /* for each cookie */
 
-		/* There's no more cookie on this line.
-		 * We may have marked the last one(s) for deletion.
-		 * We must do this now in two ways :
-		 *  - if there is no app cookie, we simply delete the header ;
-		 *  - if there are app cookies, we must delete the end of the
-		 *    string properly, including the colon/semi-colon before
-		 *    the cookie name.
+		/* There are no more cookies on this line.
+		 * We may still have one (or several) marked for deletion at the
+		 * end of the line. We must do this now in two ways :
+		 *  - if some cookies must be preserved, we only delete from the
+		 *    mark to the end of line ;
+		 *  - if nothing needs to be preserved, simply delete the whole header
 		 */
-		if (del_cookie != NULL) {
+		if (del_from) {
 			int delta;
-			if (app_cookies) {
-				delta = buffer_replace2(req, del_colon, cur_end, NULL, 0);
-				cur_end = del_colon;
+			if (preserve_hdr) {
+				delta = del_hdr_value(req, &del_from, hdr_end);
+				hdr_end = del_from;
 				cur_hdr->len += delta;
 			} else {
-				delta = buffer_replace2(req, cur_ptr, cur_next, NULL, 0);
+				delta = buffer_replace2(req, hdr_beg, hdr_next, NULL, 0);
 
 				/* FIXME: this should be a separate function */
 				txn->hdr_idx.v[old_idx].next = cur_hdr->next;
 				txn->hdr_idx.used--;
 				cur_hdr->len = 0;
 			}
-			cur_next += delta;
+			hdr_next += delta;
 			http_msg_move_end(&txn->req, delta);
 		}
 
-		/* keep the link from this header to next one */
+		/* check next header */
 		old_idx = cur_idx;
-	} /* end of cookie processing on this header */
+	}
 }
 
 
diff --git a/tests/test-cookie-appsess.cfg b/tests/test-cookie-appsess.cfg
new file mode 100644
index 0000000..61e9dbf
--- /dev/null
+++ b/tests/test-cookie-appsess.cfg
@@ -0,0 +1,36 @@
+# Test configuration. It listens on port 8000, forwards to
+# local ports 8001/8002 as two distinct servers, and relies
+# on a server running on local port 8080 to handle the request.
+
+global
+	maxconn 500
+	stats socket /tmp/sock1 mode 777 level admin
+	stats timeout 1d
+
+defaults
+	mode	http
+	option  http-server-close
+	timeout	client 30s
+	timeout server 30s
+	timeout connect 5s
+
+listen test
+	log 127.0.0.1 local0
+	option httplog
+	bind :8000
+	appsession AS len 64 timeout 3h
+	cookie SID insert indirect
+	server s1 127.0.0.1:8001 cookie s1
+	server s2 127.0.0.1:8002 cookie s2
+	capture cookie toto= len 10
+
+listen s1
+	bind 127.0.0.1:8001
+	server srv 127.0.0.1:8080
+	reqadd x-haproxy-used:\ s1
+
+listen s2
+	bind 127.0.0.1:8002
+	server srv 127.0.0.1:8080
+	reqadd x-haproxy-used:\ s2
+
diff --git a/tests/test-cookie-indirect.cfg b/tests/test-cookie-indirect.cfg
new file mode 100644
index 0000000..a6f0698
--- /dev/null
+++ b/tests/test-cookie-indirect.cfg
@@ -0,0 +1,47 @@
+# Test configuration. It listens on port 8000, forwards to
+# local ports 8001/8002 as two distinct servers, and relies
+# on a server running on local port 8080 to handle the request.
+
+# Example of request that must be handled (taken from RFC2965 and mangled
+# a bit) :
+# POST /acme/process HTTP/1.1
+# Cookie: $Version="1";
+#        Customer="WILE_E_COYOTE"; $Path="/acme";
+#     SID= s2 ; $Path="/";
+#                Part_Number="Rocket_Launcher_0001"; $Path="/acme";
+#                Shipping="FedEx"; $Path="/acme"
+#
+#
+#
+
+global
+	maxconn 500
+	stats socket /tmp/sock1 mode 777 level admin
+	stats timeout 1d
+
+defaults
+	mode	http
+	option  http-server-close
+	timeout	client 30s
+	timeout server 30s
+	timeout connect 5s
+
+listen test
+	log 127.0.0.1 local0
+	option httplog
+	bind :8000
+	cookie SID insert indirect
+	server s1 127.0.0.1:8001 cookie s1
+	server s2 127.0.0.1:8002 cookie s2
+	capture cookie toto= len 10
+
+listen s1
+	bind 127.0.0.1:8001
+	server srv 127.0.0.1:8080
+	reqadd x-haproxy-used:\ s1
+
+listen s2
+	bind 127.0.0.1:8002
+	server srv 127.0.0.1:8080
+	reqadd x-haproxy-used:\ s2
+
diff --git a/tests/test-cookie-insert.cfg b/tests/test-cookie-insert.cfg
new file mode 100644
index 0000000..70ce9b0
--- /dev/null
+++ b/tests/test-cookie-insert.cfg
@@ -0,0 +1,35 @@
+# Test configuration. It listens on port 8000, forwards to
+# local ports 8001/8002 as two distinct servers, and relies
+# on a server running on local port 8080 to handle the request.
+
+global
+	maxconn 500
+	stats socket /tmp/sock1 mode 777 level admin
+	stats timeout 1d
+
+defaults
+	mode	http
+	option  http-server-close
+	timeout	client 30s
+	timeout server 30s
+	timeout connect 5s
+
+listen test
+	log 127.0.0.1 local0
+	option httplog
+	bind :8000
+	cookie SID insert
+	server s1 127.0.0.1:8001 cookie s1
+	server s2 127.0.0.1:8002 cookie s2
+	capture cookie toto= len 10
+
+listen s1
+	bind 127.0.0.1:8001
+	server srv 127.0.0.1:8080
+	reqadd x-haproxy-used:\ s1
+
+listen s2
+	bind 127.0.0.1:8002
+	server srv 127.0.0.1:8080
+	reqadd x-haproxy-used:\ s2
+
diff --git a/tests/test-cookie-passive.cfg b/tests/test-cookie-passive.cfg
new file mode 100644
index 0000000..7533699
--- /dev/null
+++ b/tests/test-cookie-passive.cfg
@@ -0,0 +1,35 @@
+# Test configuration. It listens on port 8000, forwards to
+# local ports 8001/8002 as two distinct servers, and relies
+# on a server running on local port 8080 to handle the request.
+
+global
+	maxconn 500
+	stats socket /tmp/sock1 mode 777 level admin
+	stats timeout 1d
+
+defaults
+	mode	http
+	option  http-server-close
+	timeout	client 30s
+	timeout server 30s
+	timeout connect 5s
+
+listen test
+	log 127.0.0.1 local0
+	option httplog
+	bind :8000
+	cookie SID
+	server s1 127.0.0.1:8001 cookie s1
+	server s2 127.0.0.1:8002 cookie s2
+	capture cookie toto= len 10
+
+listen s1
+	bind 127.0.0.1:8001
+	server srv 127.0.0.1:8080
+	reqadd x-haproxy-used:\ s1
+
+listen s2
+	bind 127.0.0.1:8002
+	server srv 127.0.0.1:8080
+	reqadd x-haproxy-used:\ s2
+
diff --git a/tests/test-cookie-prefix.cfg b/tests/test-cookie-prefix.cfg
new file mode 100644
index 0000000..f000521
--- /dev/null
+++ b/tests/test-cookie-prefix.cfg
@@ -0,0 +1,35 @@
+# Test configuration. It listens on port 8000, forwards to
+# local ports 8001/8002 as two distinct servers, and relies
+# on a server running on local port 8080 to handle the request.
+
+global
+	maxconn 500
+	stats socket /tmp/sock1 mode 777 level admin
+	stats timeout 1d
+
+defaults
+	mode	http
+	option  http-server-close
+	timeout	client 30s
+	timeout server 30s
+	timeout connect 5s
+
+listen test
+	log 127.0.0.1 local0
+	option httplog
+	bind :8000
+	cookie SID prefix 
+	server s1 127.0.0.1:8001 cookie s1
+	server s2 127.0.0.1:8002 cookie s2
+	capture cookie toto= len 10
+
+listen s1
+	bind 127.0.0.1:8001
+	server srv 127.0.0.1:8080
+	reqadd x-haproxy-used:\ s1
+
+listen s2
+	bind 127.0.0.1:8002
+	server srv 127.0.0.1:8080
+	reqadd x-haproxy-used:\ s2
+
diff --git a/tests/test-cookie-rewrite.cfg b/tests/test-cookie-rewrite.cfg
new file mode 100644
index 0000000..87b3f7f
--- /dev/null
+++ b/tests/test-cookie-rewrite.cfg
@@ -0,0 +1,35 @@
+# Test configuration. It listens on port 8000, forwards to
+# local ports 8001/8002 as two distinct servers, and relies
+# on a server running on local port 8080 to handle the request.
+
+global
+	maxconn 500
+	stats socket /tmp/sock1 mode 777 level admin
+	stats timeout 1d
+
+defaults
+	mode	http
+	option  http-server-close
+	timeout	client 30s
+	timeout server 30s
+	timeout connect 5s
+
+listen test
+	log 127.0.0.1 local0
+	option httplog
+	bind :8000
+	cookie SID rewrite
+	server s1 127.0.0.1:8001 cookie s1
+	server s2 127.0.0.1:8002 cookie s2
+	capture cookie toto= len 10
+
+listen s1
+	bind 127.0.0.1:8001
+	server srv 127.0.0.1:8080
+	reqadd x-haproxy-used:\ s1
+
+listen s2
+	bind 127.0.0.1:8002
+	server srv 127.0.0.1:8080
+	reqadd x-haproxy-used:\ s2
+