--- jakarta-tomcat-catalina/catalina/src/share/org/apache/coyote/tomcat5/CoyoteRequest.java	2009-04-20 17:29:42.000000000 +0200
+++ jakarta-tomcat-catalina/catalina/src/share/org/apache/coyote/tomcat5/CoyoteRequest.java	2009-04-21 09:41:12.000000000 +0200
@@ -2312,6 +2312,22 @@
         }
     }
 
+    protected String unescape(String s) {
+        if (s==null) return null;
+        if (s.indexOf('\\') == -1) return s;
+        StringBuffer buf = new StringBuffer();
+        for (int i=0; i<s.length(); i++) {
+            char c = s.charAt(i);
+            if (c!='\\') buf.append(c);
+            else {
+                if (++i >= s.length()) throw new IllegalArgumentException();//invalid escape, hence invalid cookie
+                c = s.charAt(i);
+                buf.append(c);
+            }
+        }
+        return buf.toString();
+    }
+    
     /**
      * Parse cookies.
      */
@@ -2330,14 +2346,18 @@
         for (int i = 0; i < count; i++) {
             ServerCookie scookie = serverCookies.getCookie(i);
             try {
-                Cookie cookie = new Cookie(scookie.getName().toString(),
-                                           scookie.getValue().toString());
-                cookie.setPath(scookie.getPath().toString());
-                cookie.setVersion(scookie.getVersion());
+                /*
+                we must unescape the '\\' escape character
+                */
+                Cookie cookie = new Cookie(scookie.getName().toString(),null);
+                int version = scookie.getVersion();
+                cookie.setVersion(version);
+                cookie.setValue(unescape(scookie.getValue().toString()));
+                cookie.setPath(unescape(scookie.getPath().toString()));
                 String domain = scookie.getDomain().toString();
-                if (domain != null) {
-                    cookie.setDomain(scookie.getDomain().toString());
-                }
+                if (domain!=null) cookie.setDomain(unescape(domain));//avoid NPE
+                String comment = scookie.getComment().toString();
+                cookie.setComment(version==1?unescape(comment):null);
                 cookies[idx++] = cookie;
             } catch(IllegalArgumentException e) {
                 // Ignore bad cookie
--- jakarta-tomcat-catalina/catalina/src/share/org/apache/coyote/tomcat5/CoyoteResponse.java	2004-11-24 17:55:18.000000000 +0100
+++ jakarta-tomcat-catalina/catalina/src/share/org/apache/coyote/tomcat5/CoyoteResponse.java	2009-04-21 09:41:12.000000000 +0200
@@ -932,9 +932,9 @@
         if (included)
             return;
 
-        cookies.add(cookie);
-
         final StringBuffer sb = new StringBuffer();
+        //web application code can receive a IllegalArgumentException 
+        //from the appendCookieValue invokation
         if (SecurityUtil.isPackageProtectionEnabled()) {
             AccessController.doPrivileged(new PrivilegedAction() {
                 public Object run(){
@@ -953,11 +953,13 @@
                      cookie.getMaxAge(), cookie.getSecure());
         }
 
+        // if we reached here, no exception, cookie is valid
         // the header name is Set-Cookie for both "old" and v.1 ( RFC2109 )
         // RFC2965 is not supported by browsers and the Servlet spec
         // asks for 2109.
         addHeader("Set-Cookie", sb.toString());
 
+        cookies.add(cookie);
     }
 
 
--- jakarta-tomcat-catalina/webapps/docs/changelog.xml	2004-11-24 17:55:37.000000000 +0100
+++ jakarta-tomcat-catalina/webapps/docs/changelog.xml	2009-04-21 09:56:50.000000000 +0200
@@ -79,6 +79,18 @@
       <fix>
         <bug>32269</bug>: JNDIRealm fails with InvalidNameException to authenticate users if LDAP distinguished name (DN) contains slash or double quote character(s). (yoavs)
       </fix>
+      <fix>
+        Cookie handling/parsing changes!
+        The following behavior has been changed with regards to Tomcat's cookie
+        handling:<br/>
+        a) Cookies containing control characters, except 0x09(HT), are rejected
+        using an InvalidArgumentException.<br/>
+        b) If cookies are not quoted, they will be quoted if they contain
+        <code>tspecials(ver0)</code> or <code>tspecials2(ver1)</code>
+        characters.<br/>
+        c) Escape character '\\' is allowed and respected as a escape character,
+        and will be unescaped during parsing.
+      </fix>
     </changelog>
   </subsection>
 
--- jakarta-tomcat-connectors/util/java/org/apache/tomcat/util/http/Cookies.java	2009-04-20 17:29:42.000000000 +0200
+++ jakarta-tomcat-connectors/util/java/org/apache/tomcat/util/http/Cookies.java	2009-04-20 17:49:17.000000000 +0200
@@ -41,6 +41,27 @@
     boolean unprocessed=true;
 
     MimeHeaders headers;
+
+    /*
+    List of Separator Characters (see isSeparator())
+    Excluding the '/' char violates the RFC, but 
+    it looks like a lot of people put '/'
+    in unquoted values: '/': ; //47 
+    '\t':9 ' ':32 '\"':34 '\'':39 '(':40 ')':41 ',':44 ':':58 ';':59 '<':60 
+    '=':61 '>':62 '?':63 '@':64 '[':91 '\\':92 ']':93 '{':123 '}':125
+    */
+    public static final char SEPARATORS[] = { '\t', ' ', '\"', '\'', '(', ')', ',', 
+        ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}' };
+
+    protected static final boolean separators[] = new boolean[128];
+    static {
+        for (int i = 0; i < 128; i++) {
+            separators[i] = false;
+        }
+        for (int i = 0; i < SEPARATORS.length; i++) {
+            separators[SEPARATORS[i]] = true;
+        }
+    }
     
     /**
      *  Construct a new cookie collection, that will extract
@@ -175,174 +196,6 @@
 	}
     }
 
-    /** Process a byte[] header - allowing fast processing of the
-     *  raw data
-     */
-    void processCookieHeader(  byte bytes[], int off, int len )
-    {
-	if( len<=0 || bytes==null ) return;
-	int end=off+len;
-	int pos=off;
-	
-	int version=0; //sticky
-	ServerCookie sc=null;
-	
-
-	while( pos<end ) {
-	    byte cc;
-	    // [ skip_spaces name skip_spaces "=" skip_spaces value EXTRA ; ] *
-	    if( dbg>0 ) log( "Start: " + pos + " " + end );
-	    
-	    pos=skipSpaces(bytes, pos, end);
-	    if( pos>=end )
-		return; // only spaces
-	    int startName=pos;
-	    if( dbg>0 ) log( "SN: " + pos );
-	    
-	    // Version should be the first token
-	    boolean isSpecial=false;
-	    if(bytes[pos]=='$') { pos++; isSpecial=true; }
-
-	    pos= findDelim1( bytes, startName, end); // " =;,"
-	    int endName=pos;
-	    // current = "=" or " " or DELIM
-	    pos= skipSpaces( bytes, endName, end ); 
-	    if( dbg>0 ) log( "DELIM: " + endName + " " + (char)bytes[pos]);
-
-	    if(pos >= end ) {
-		// it's a name-only cookie ( valid in RFC2109 )
-		if( ! isSpecial ) {
-		    sc=addCookie();
-		    sc.getName().setBytes( bytes, startName,
-					   endName-startName );
-		    sc.getValue().setString("");
-		    sc.setVersion( version );
-		    if( dbg>0 ) log( "Name only, end: " + startName + " " +
-				     endName);
-		}
-		return;
-	    }
-
-	    cc=bytes[pos];
-	    pos++;
-	    if( cc==';' || cc==',' ) {
-		if( ! isSpecial && startName!= endName ) {
-		    sc=addCookie();
-		    sc.getName().setBytes( bytes, startName,
-					   endName-startName );
-		    sc.getValue().setString("");
-		    sc.setVersion( version );
-		    if( dbg>0 ) log( "Name only: " + startName + " " + endName);
-		}
-		continue;
-	    }
-	    
-	    // we should have "=" ( tested all other alternatives )
-	    int startValue=skipSpaces( bytes, pos, end);
-	    int endValue=startValue;
-	    
-	    // quote is valid only in version=1 cookies
-	    cc=bytes[pos];
-	    if( ( version == 1 || isSpecial ) && ( cc== '"' ) ) {
-                endValue=findDelim3( bytes, startValue+1, end, cc );
-                if (endValue == -1) {
-                    endValue = findDelim2(bytes, startValue+1, end);
-                } else startValue++;
-		pos=endValue+1; // to skip to next cookie
- 	    } else {
-		endValue=findDelim2( bytes, startValue, end );
-		pos=endValue+1;
-	    }
-	    
-	    // if not $Version, etc
-	    if( ! isSpecial ) {
-		sc=addCookie();
-		sc.getName().setBytes( bytes, startName, endName-startName );
-		sc.getValue().setBytes( bytes, startValue, endValue-startValue);
-		sc.setVersion( version );
-		if( dbg>0 ) log( "New: " + sc.getName() + "X=X" + sc.getValue());
-		continue;
-	    }
-	    
-	    // special - Path, Version, Domain, Port
-	    if( dbg>0 ) log( "Special: " + startName + " " + endName);
-	    // XXX TODO
-	    if( equals( "$Version", bytes, startName, endName ) ) {
-		if(dbg>0 ) log( "Found version " );
-		if( bytes[startValue]=='1' && endValue==startValue+1 ) {
-		    version=1;
-		    if(dbg>0 ) log( "Found version=1" );
-		}
-		continue;
-	    }
-	    if( sc==null ) {
-		// Path, etc without a previous cookie
-		continue;
-	    }
-	    if( equals( "$Path", bytes, startName, endName ) ) {
-		sc.getPath().setBytes( bytes, startValue, endValue-startValue );
-	    }
-	    if( equals( "$Domain", bytes, startName, endName ) ) {
-		sc.getDomain().setBytes( bytes, startValue, endValue-startValue );
-	    }
-	    if( equals( "$Port", bytes, startName, endName ) ) {
-		// sc.getPort().setBytes( bytes, startValue, endValue-startValue );
-	    }
-	}
-    }
-
-    // -------------------- Utils --------------------
-    public static int skipSpaces(  byte bytes[], int off, int end ) {
-	while( off < end ) {
-	    byte b=bytes[off];
-	    if( b!= ' ' ) return off;
-	    off ++;
-	}
-	return off;
-    }
-
-    public static int findDelim1( byte bytes[], int off, int end )
-    {
-	while( off < end ) {
-	    byte b=bytes[off];
-	    if( b==' ' || b=='=' || b==';' || b==',' )
-		return off;
-	    off++;
-	}
-	return off;
-    }
-
-    public static int findDelim2( byte bytes[], int off, int end )
-    {
-	while( off < end ) {
-	    byte b=bytes[off];
-	    if( b==';' || b==',' )
-		return off;
-	    off++;
-	}
-	return off;
-    }
-
-    /*
-     *  search for cc but skip \cc as required by rfc2616
-     *  (according to rfc2616 cc should be ")
-     */
-    public static int findDelim3( byte bytes[], int off, int end, byte cc )
-    {
-        while( off < end ) {
-            byte b=bytes[off];
-            if (b=='\\') {
-                off++;
-                off++;
-                continue;
-            }
-            if( b==cc )
-                return off;
-            off++;
-        }
-        return -1;
-    }
-
     // XXX will be refactored soon!
     public static boolean equals( String s, byte b[], int start, int end) {
 	int blen = end-start;
@@ -398,7 +251,7 @@
     /**
      *
      * Strips quotes from the start and end of the cookie string
-     * This conforms to RFC 2109
+     * This conforms to RFC 2965
      * 
      * @param value            a <code>String</code> specifying the cookie 
      *                         value (possibly quoted).
@@ -409,8 +262,7 @@
     private static String stripQuote( String value )
     {
 	//	log("Strip quote from " + value );
-	if (((value.startsWith("\"")) && (value.endsWith("\""))) ||
-	    ((value.startsWith("'") && (value.endsWith("'"))))) {
+	if (value.startsWith("\"") && value.endsWith("\"")) {
 	    try {
 		return value.substring(1,value.length()-1);
 	    } catch (Exception ex) { 
@@ -426,42 +278,299 @@
 	System.out.println("Cookies: " + s);
     }
 
-    /*
-    public static void main( String args[] ) {
-	test("foo=bar; a=b");
-	test("foo=bar;a=b");
-	test("foo=bar;a=b;");
-	test("foo=bar;a=b; ");
-	test("foo=bar;a=b; ;");
-	test("foo=;a=b; ;");
-	test("foo;a=b; ;");
-	// v1 
-	test("$Version=1; foo=bar;a=b"); 
-        test("$Version=\"1\"; foo='bar'; $Path=/path; $Domain=\"localhost\"");
-	test("$Version=1;foo=bar;a=b; ; ");
-	test("$Version=1;foo=;a=b; ; ");
-	test("$Version=1;foo= ;a=b; ; ");
-	test("$Version=1;foo;a=b; ; ");
-	test("$Version=1;foo=\"bar\";a=b; ; ");
-	test("$Version=1;foo=\"bar\";$Path=/examples;a=b; ; ");
-	test("$Version=1;foo=\"bar\";$Domain=apache.org;a=b");
-	test("$Version=1;foo=\"bar\";$Domain=apache.org;a=b;$Domain=yahoo.com");
-	// rfc2965
-	test("$Version=1;foo=\"bar\";$Domain=apache.org;$Port=8080;a=b");
-
-	// wrong
-	test("$Version=1;foo=\"bar\";$Domain=apache.org;$Port=8080;a=b");
-    }
-
-    public static void test( String s ) {
-	System.out.println("Processing " + s );
-	Cookies cs=new Cookies(null);
-	cs.processCookieHeader( s.getBytes(), 0, s.length());
-	for( int i=0; i< cs.getCookieCount() ; i++ ) {
-	    System.out.println("Cookie: " + cs.getCookie( i ));
-	}
-	    
+  
+   /**
+     * Returns true if the byte is a separator character as
+     * defined in RFC2619. Since this is called often, this
+     * function should be organized with the most probable
+     * outcomes first.
+     */
+    public static final boolean isSeparator(final byte c) {
+         if (c > 0 && c < 126)
+             return separators[c];
+         else
+             return false;
     }
-    */
+    
+    /**
+     * Returns true if the byte is a whitespace character as
+     * defined in RFC2619.
+     */
+    public static final boolean isWhiteSpace(final byte c) {
+        // This switch statement is slightly slower
+        // for my vm than the if statement.
+        // Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_07-164)
+        /* 
+        switch (c) {
+        case ' ':;
+        case '\t':;
+        case '\n':;
+        case '\r':;
+        case '\f':;
+            return true;
+        default:;
+            return false;
+             }
+        */
+       if (c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f')
+           return true;
+       else
+           return false;
+    }
+
+    /**
+     * Parses a cookie header after the initial "Cookie:"
+     * [WS][$]token[WS]=[WS](token|QV)[;|,]
+     * RFC 2965
+     * JVK
+     */
+    public final void processCookieHeader(byte bytes[], int off, int len){
+        if( len<=0 || bytes==null ) return;
+        int end=off+len;
+        int pos=off;
+        int nameStart=0;
+        int nameEnd=0;
+        int valueStart=0;
+        int valueEnd=0;
+        int version = 0;
+        ServerCookie sc=null;
+        boolean isSpecial;
+        boolean isQuoted;
+
+        while (pos < end) {
+            isSpecial = false;
+            isQuoted = false;
+
+            // Skip whitespace and non-token characters (separators)
+            while (pos < end && 
+                   (isSeparator(bytes[pos]) || isWhiteSpace(bytes[pos]))) 
+                {pos++; } 
+
+            if (pos >= end)
+                return;
+
+            // Detect Special cookies
+            if (bytes[pos] == '$') {
+                isSpecial = true;
+                pos++;
+            }
 
+            // Get the cookie name. This must be a token            
+            valueEnd = valueStart = nameStart = pos; 
+            pos = nameEnd = getTokenEndPosition(bytes,pos,end);
+
+            // Skip whitespace
+            while (pos < end && isWhiteSpace(bytes[pos])) {pos++; }; 
+         
+
+            // Check for an '=' -- This could also be a name-only
+            // cookie at the end of the cookie header, so if we
+            // are past the end of the header, but we have a name
+            // skip to the name-only part.
+            if (pos < end && bytes[pos] == '=') {                
+
+                // Skip whitespace
+                do {
+                    pos++;
+                } while (pos < end && isWhiteSpace(bytes[pos])); 
+
+                if (pos >= end)
+                    return;
+
+                // Determine what type of value this is, quoted value,
+                // token, name-only with an '=', or other (bad)
+                switch (bytes[pos]) {
+                case '"':; // Quoted Value
+                    isQuoted = true;
+                    valueStart=pos + 1; // strip "
+                    // getQuotedValue returns the position before 
+                    // at the last qoute. This must be dealt with
+                    // when the bytes are copied into the cookie
+                    valueEnd=getQuotedValueEndPosition(bytes, 
+                                                       valueStart, end);
+                    // We need pos to advance
+                    pos = valueEnd; 
+                    // Handles cases where the quoted value is 
+                    // unterminated and at the end of the header, 
+                    // e.g. [myname="value]
+                    if (pos >= end)
+                         return;
+                     break;
+                 case ';':
+                 case ',':
+                     // Name-only cookie with an '=' after the name token
+                     // This may not be RFC compliant
+                     valueStart = valueEnd = -1;
+                     // The position is OK (On a delimiter)
+                     break;
+                 default:;
+                     if (!isSeparator(bytes[pos])) {
+                         // Token
+                         valueStart=pos;
+                         // getToken returns the position at the delimeter
+                         // or other non-token character
+                         valueEnd=getTokenEndPosition(bytes, valueStart, end);
+                         // We need pos to advance
+                         pos = valueEnd;
+                     } else  {
+                         // INVALID COOKIE, advance to next delimiter
+                         // The starting character of the cookie value was
+                         // not valid.
+                         log("Invalid cookie. Value not a token or quoted value");
+                         while (pos < end && bytes[pos] != ';' && 
+                                bytes[pos] != ',') 
+                             {pos++; };
+                         pos++;
+                         // Make sure no special avpairs can be attributed to 
+                         // the previous cookie by setting the current cookie
+                         // to null
+                         sc = null;
+                         continue;                        
+                     }
+                 }
+             } else {
+                 // Name only cookie
+                 valueStart = valueEnd = -1;
+                 pos = nameEnd;
+ 
+             }
+           
+             // We should have an avpair or name-only cookie at this
+             // point. Perform some basic checks to make sure we are
+             // in a good state.
+   
+             // Skip whitespace
+             while (pos < end && isWhiteSpace(bytes[pos])) {pos++; }; 
+ 
+ 
+             // Make sure that after the cookie we have a separator. This
+             // is only important if this is not the last cookie pair
+             while (pos < end && bytes[pos] != ';' && bytes[pos] != ',') { 
+                 pos++;
+             }
+                  
+             pos++;
+ 
+             /*
+             if (nameEnd <= nameStart || valueEnd < valueStart ) {
+                 // Something is wrong, but this may be a case
+                 // of having two ';' characters in a row.
+                 // log("Cookie name/value does not conform to RFC 2965");
+                 // Advance to next delimiter (ignoring everything else)
+                 while (pos < end && bytes[pos] != ';' && bytes[pos] != ',') 
+                     { pos++; };
+                 pos++;
+                 // Make sure no special cookies can be attributed to 
+                 // the previous cookie by setting the current cookie
+                 // to null
+                 sc = null;
+                 continue;
+             }
+             */
+ 
+             // All checks passed. Add the cookie, start with the 
+             // special avpairs first
+             if (isSpecial) {
+                 isSpecial = false;
+                 // $Version must be the first avpair in the cookie header
+                 // (sc must be null)
+                 if (equals( "Version", bytes, nameStart, nameEnd) && 
+                     sc == null) {
+                     // Set version
+                     if( bytes[valueStart] =='1' && valueEnd == (valueStart+1)) {
+                         version=1;
+                     } else {
+                         // unknown version (Versioning is not very strict)
+                     }
+                     continue;
+                 } 
+                 
+                 // We need an active cookie for Path/Port/etc.
+                 if (sc == null) {
+                     continue;
+                 }
+ 
+                 // Domain is more common, so it goes first
+                 if (equals( "Domain", bytes, nameStart, nameEnd)) {
+                     sc.getDomain().setBytes( bytes,
+                                            valueStart,
+                                            valueEnd-valueStart);
+                     continue;
+                 } 
+ 
+                 if (equals( "Path", bytes, nameStart, nameEnd)) {
+                     sc.getPath().setBytes( bytes,
+                                            valueStart,
+                                            valueEnd-valueStart);
+                     continue;
+                 } 
+ 
+ 
+                 if (equals( "Port", bytes, nameStart, nameEnd)) {
+                     // sc.getPort is not currently implemented.
+                     // sc.getPort().setBytes( bytes,
+                     //                        valueStart,
+                     //                        valueEnd-valueStart );
+                     continue;
+                 } 
+ 
+                 // Unknown cookie, complain
+                 log("Unknown Special Cookie");
+ 
+             } else { // Normal Cookie
+                 sc = addCookie();
+                 sc.setVersion( version );
+                 sc.getName().setBytes( bytes, nameStart,
+                                        nameEnd-nameStart);
+                 
+                 if (valueStart != -1) { // Normal AVPair
+                     sc.getValue().setBytes( bytes, valueStart,
+                             valueEnd-valueStart);
+                     if (isQuoted) {
+                         // We know this is a byte value so this is safe
+                         ServerCookie.unescapeDoubleQuotes(
+                                 sc.getValue().getByteChunk());
+                     }                    
+                 } else {
+                     // Name Only
+                     sc.getValue().setString(""); 
+                 }
+                 continue;
+             }
+          }
+      }
+  
+     /**
+      * Given the starting position of a token, this gets the end of the
+      * token, with no separator characters in between.
+      * JVK
+      */
+     public static final int getTokenEndPosition(byte bytes[], int off, int end){
+         int pos = off;
+         while (pos < end && !isSeparator(bytes[pos])) {pos++; };
+         
+         if (pos > end)
+             return end;
+         return pos;
+     }
+ 
+     /** 
+      * Given a starting position after an initial quote chracter, this gets
+      * the position of the end quote. This escapes anything after a '\' char
+      * JVK RFC 2616
+      */
+     public static final int getQuotedValueEndPosition(byte bytes[], int off, int end){
+         int pos = off;
+         while (pos < end) {
+             if (bytes[pos] == '"') {
+                 return pos;                
+             } else if (bytes[pos] == '\\' && pos < (end - 1)) {
+                 pos+=2;
+             } else {
+                 pos++;
+             }
+         }
+         // Error, we have reached the end of the header w/o a end quote
+         return end;
+     }
 }
--- jakarta-tomcat-connectors/util/java/org/apache/tomcat/util/http/ServerCookie.java	2009-04-20 17:29:42.000000000 +0200
+++ jakarta-tomcat-connectors/util/java/org/apache/tomcat/util/http/ServerCookie.java	2009-04-20 18:34:27.000000000 +0200
@@ -16,6 +16,7 @@
 
 package org.apache.tomcat.util.http;
 
+import org.apache.tomcat.util.buf.ByteChunk;
 import org.apache.tomcat.util.buf.MessageBytes;
 import org.apache.tomcat.util.buf.DateTool;
 import java.text.*;
@@ -47,6 +48,9 @@
     private int version = 0;	// ;Version=1
 
     //XXX CommentURL, Port -> use notes ?
+
+    public static final boolean VERSION_SWITCH =
+        Boolean.valueOf(System.getProperty("org.apache.tomcat.util.http.ServerCookie.VERSION_SWITCH", "true")).booleanValue();
     
     public ServerCookie() {
 
@@ -80,7 +84,6 @@
 	return maxAge;
     }
 
-
     public MessageBytes getPath() {
 	return path;
     }
@@ -105,7 +108,6 @@
 	return version;
     }
 
-
     public void setVersion(int v) {
 	version = v;
     }
@@ -122,8 +124,9 @@
     // from RFC 2068, token special case characters
     //
     // private static final String tspecials = "()<>@,;:\\\"/[]?={} \t";
-    private static final String tspecials = ",;";
-    private static final String tspecials2 = ",; \"";
+    private static final String tspecials = ",; ";
+    private static final String tspecials2 = "()<>@,;:\\\"/[]?={} \t";
+    private static final String tspecials2NoSlash = "()<>@,;:\\\"[]?={} \t";
 
     /*
      * Tests a string and returns true if the string counts as a
@@ -136,26 +139,52 @@
      *				if it is not
      */
     public static boolean isToken(String value) {
+        return isToken(value,null);
+    }
+    
+    public static boolean isToken(String value, String literals) {
+        String tspecials = (literals==null?ServerCookie.tspecials:literals);
+
 	if( value==null) return true;
 	int len = value.length();
 
 	for (int i = 0; i < len; i++) {
 	    char c = value.charAt(i);
 
-	    if (c < 0x20 || c >= 0x7f || tspecials.indexOf(c) != -1)
+	    if (tspecials.indexOf(c) != -1)
 		return false;
 	}
 	return true;
     }
 
+    public static boolean containsCTL(String value, int version) {
+        if( value==null) return false;
+        int len = value.length();
+        for (int i = 0; i < len; i++) {
+            char c = value.charAt(i);
+            if (c < 0x20 || c >= 0x7f) {
+                if (c == 0x09)
+                    continue; //allow horizontal tabs
+                return true;
+            }
+        }
+        return false;
+    }
+
     public static boolean isToken2(String value) {
+        return isToken2(value,null);
+    }
+
+    public static boolean isToken2(String value, String literals) {
+        String tspecials2 = (literals==null?ServerCookie.tspecials2:literals);
+
         if( value==null) return true;
         int len = value.length();
 
         for (int i = 0; i < len; i++) {
             char c = value.charAt(i);
 
-            if (c < 0x20 || c >= 0x7f || tspecials2.indexOf(c) != -1)
+            if (tspecials2.indexOf(c) != -1)
                 return false;
         }
         return true;
@@ -181,8 +210,8 @@
     // -------------------- Cookie parsing tools
 
     
-    /** Return the header name to set the cookie, based on cookie
-     *  version
+    /**
+     * Return the header name to set the cookie, based on cookie version.
      */
     public String getCookieHeaderName() {
 	return getCookieHeaderName(version);
@@ -192,7 +221,6 @@
      *  version
      */
     public static String getCookieHeaderName(int version) {
-	if( dbg>0 ) log( (version==1) ? "Set-Cookie2" : "Set-Cookie");
         if (version == 1) {
 	    // RFC2109
 	    return "Set-Cookie";
@@ -208,7 +236,7 @@
 
     private static final String ancientDate=DateTool.formatOldCookie(new Date(10000));
 
-    public static void appendCookieValue( StringBuffer buf,
+    public static void appendCookieValue( StringBuffer headerBuf,
 					  int version,
 					  String name,
 					  String value,
@@ -219,9 +247,10 @@
 					  boolean isSecure )
     {
         // this part is the same for all cookies
+        StringBuffer buf = new StringBuffer();
 	buf.append( name );
         buf.append("=");
-        maybeQuote2(version, buf, value);
+        version = maybeQuote2(version, buf, value, true);
 
 	// XXX Netscape cookie: "; "
  	// add version 1 specific information
@@ -232,7 +261,7 @@
 	    // Comment=comment
 	    if ( comment!=null ) {
 		buf.append ("; Comment=");
-		maybeQuote (version, buf, comment);
+		maybeQuote2 (version, buf, comment);
 	    }
 	}
 	
@@ -240,7 +269,7 @@
 
 	if (domain!=null) {
 	    buf.append("; Domain=");
-	    maybeQuote (version, buf, domain);
+	    maybeQuote2 (version, buf, domain);
 	}
 
 	// Max-Age=secs/Discard ... or use old "Expires" format
@@ -269,14 +298,18 @@
 	// Path=path
 	if (path!=null) {
 	    buf.append ("; Path=");
-	    maybeQuote (version, buf, path);
+            if (version==0) {
+                maybeQuote2(version, buf, path);
+            } else {
+                maybeQuote2(version, buf, path, ServerCookie.tspecials2NoSlash, false);
+            }
 	}
 
 	// Secure
 	if (isSecure) {
 	  buf.append ("; Secure");
 	}
-	
+        headerBuf.append(buf);
 	
     }
 
@@ -291,25 +324,52 @@
 		throw new IllegalArgumentException( value );
 	    else {
 		buf.append ('"');
-		buf.append (escapeDoubleQuotes(value));
+		buf.append(escapeDoubleQuotes(value,0,value.length()));
 		buf.append ('"');
 	    }
 	}
     }
 
-    public static void maybeQuote2 (int version, StringBuffer buf,
-            String value) {
-        // special case - a \n or \r  shouldn't happen in any case
-        if (isToken2(value)) {
-            buf.append(value);
-        } else {
+    public static boolean alreadyQuoted (String value) {
+        if (value==null || value.length()==0) return false;
+        return (value.charAt(0)=='\"' && value.charAt(value.length()-1)=='\"');
+    }
+
+    public static int maybeQuote2(int version, StringBuffer buf, String value) {
+        return maybeQuote2(version,buf,value,false);
+    }
+    public static int maybeQuote2 (int version, StringBuffer buf, String value, boolean allowVersionSwitch) {
+        return maybeQuote2(version,buf,value,null,allowVersionSwitch);
+    }
+
+    public static int maybeQuote2 (int version, StringBuffer buf, String value, String literals, boolean allowVersionSwitch) {
+        if (value==null || value.length()==0) {
+            buf.append("\"\"");
+        } else if (containsCTL(value,version))
+            throw new IllegalArgumentException("Control character in cookie value, consider BASE64 encoding your value");
+        else if (alreadyQuoted(value)) {
+            buf.append('"');
+            buf.append(escapeDoubleQuotes(value,1,value.length()-1));
+            buf.append('"');
+        } else if (allowVersionSwitch && VERSION_SWITCH && version==0 && !isToken2(value, literals)) {
             buf.append('"');
-            buf.append(escapeDoubleQuotes(value));
+            buf.append(escapeDoubleQuotes(value,0,value.length()));
             buf.append('"');
+            version = 1;
+        } else if (version==0 && !isToken(value, literals)) {
+            buf.append('"');
+            buf.append(escapeDoubleQuotes(value,0,value.length()));
+            buf.append('"');
+        } else if (version==1 && !isToken2(value, literals)) {
+            buf.append('"');
+            buf.append(escapeDoubleQuotes(value,0,value.length()));
+            buf.append('"');
+        } else {
+            buf.append(value);
         }
+        return version;
     }
 
-
     // log
     static final int dbg=1;
     public static void log(String s ) {
@@ -323,25 +383,55 @@
      *
      * @return The (possibly) escaped string
      */
-    private static String escapeDoubleQuotes(String s) {
+    private static String escapeDoubleQuotes(String s, int beginIndex,
+            int endIndex) {
 
         if (s == null || s.length() == 0 || s.indexOf('"') == -1) {
             return s;
         }
 
         StringBuffer b = new StringBuffer();
-        char p = s.charAt(0);
-        for (int i = 0; i < s.length(); i++) {
+        for (int i = beginIndex; i < endIndex; i++) {
             char c = s.charAt(i);
-            if (c == '"' && p != '\\')
+            if (c == '\\' ) {
+                b.append(c);
+                //ignore the character after an escape, just append it
+                if (++i>=endIndex) throw new IllegalArgumentException("Invalid escape character in cookie value.");
+                b.append(s.charAt(i));
+            } else if (c == '"')
                 b.append('\\').append('"');
             else
                 b.append(c);
-            p = c;
         }
 
         return b.toString();
     }
+    /**
+     * Unescapes any double quotes in the given cookie value.
+     *
+     * @param bc The cookie value to modify
+     */
+    public static void unescapeDoubleQuotes(ByteChunk bc) {
+
+        if (bc == null || bc.getLength() == 0 || bc.indexOf('"', 0) == -1) {
+            return;
+        }
+
+        int src = bc.getStart();
+        int end = bc.getEnd();
+        int dest = src;
+        byte[] buffer = bc.getBuffer();
+
+        while (src < end) {
+            if (buffer[src] == '\\' && src < end && buffer[src+1]  == '"') {
+                src++;
+            }
+            buffer[dest] = buffer[src];
+            dest ++;
+            src ++;
+        }
+        bc.setEnd(dest);
+    }
 
 }
 
