Author: markt Date: Sat Nov 14 03:47:48 2009 New Revision: 836113 URL: http://svn.apache.org/viewvc?rev=836113&view=rev Log: More cookie refactoring - new support class for common elements of parsing and writing - better consistency between parsing and writing - remove unused code - reduce visibility of methods where possible - auto-switch to v1 for any attribute that might require quoting - better names for constants - allow v0 cookies to break http spec (disabled by default) - update test cases and documentation
Added: tomcat/trunk/java/org/apache/tomcat/util/http/CookieSupport.java (with props) tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesAllowHttpSeps.java (with props) Modified: tomcat/trunk/java/org/apache/tomcat/util/http/Cookies.java tomcat/trunk/java/org/apache/tomcat/util/http/ServerCookie.java tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesAllowEquals.java tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesDefaultSysProps.java tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesDisallowEquals.java tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesNoFwdStrictSysProps.java tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesStrictSysProps.java tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesSwitchSysProps.java tomcat/trunk/webapps/docs/config/systemprops.xml Added: tomcat/trunk/java/org/apache/tomcat/util/http/CookieSupport.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/http/CookieSupport.java?rev=836113&view=auto ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/http/CookieSupport.java (added) +++ tomcat/trunk/java/org/apache/tomcat/util/http/CookieSupport.java Sat Nov 14 03:47:48 2009 @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.tomcat.util.http; + + +/** + * Static constants for this package. + */ +public final class CookieSupport { + + + // --------------------------------------------------------------- Constants + /** + * If set to true, we parse cookies strictly according to the servlet, + * cookie and HTTP specs by default. + */ + public static final boolean STRICT_SERVLET_COMPLIANCE; + + /** + * If true, cookie values are allowed to contain an equals character without + * being quoted. + */ + public static final boolean ALLOW_EQUALS_IN_VALUE; + + /** + * If true, separators that are not explicitly dis-allowed by the v0 cookie + * spec but are disallowed by the HTTP spec will be allowed in v0 cookie + * names and values. These characters are: \"()/:<=>?...@[\\]{} Note that the + * inclusion of / depend on the value of {...@link #FWD_SLASH_IS_SEPARATOR}. + */ + public static final boolean ALLOW_HTTP_SEPARATORS_IN_V0; + + /** + * If set to false, we don't use the IE6/7 Max-Age/Expires work around. + * Default is usually true. If STRICT_SERVLET_COMPLIANCE==true then default + * is false. Explicitly setting always takes priority. + */ + public static final boolean ALWAYS_ADD_EXPIRES; + + /** + * If set to true, the <code>/</code> character will be treated as a + * separator. Default is usually false. If STRICT_SERVLET_COMPLIANCE==true + * then default is true. Explicitly setting always takes priority. + */ + public static final boolean FWD_SLASH_IS_SEPARATOR; + + /** + * The list of separators that apply to version 0 cookies. To quote the + * spec, these are comma, semi-colon and white-space. The HTTP spec + * definition of linear white space is [CRLF] 1*( SP | HT ) + */ + public static final char[] V0_SEPARATORS = {',', ';', ' ', '\t'}; + public static final boolean[] V0_SEPARATOR_FLAGS = new boolean[128]; + + /** + * The list of separators that apply to version 1 cookies. This may or may + * not include '/' depending on the setting of + * {...@link #FWD_SLASH_IS_SEPARATOR}. + */ + public static final char[] HTTP_SEPARATORS; + public static final boolean[] HTTP_SEPARATOR_FLAGS = new boolean[128]; + + static { + STRICT_SERVLET_COMPLIANCE = Boolean.valueOf(System.getProperty( + "org.apache.catalina.STRICT_SERVLET_COMPLIANCE", + "false")).booleanValue(); + + ALLOW_EQUALS_IN_VALUE = Boolean.valueOf(System.getProperty( + "org.apache.tomcat.util.http.ServerCookie.ALLOW_EQUALS_IN_VALUE", + "false")).booleanValue(); + + ALLOW_HTTP_SEPARATORS_IN_V0 = Boolean.valueOf(System.getProperty( + "org.apache.tomcat.util.http.ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0", + "false")).booleanValue(); + + String alwaysAddExpires = System.getProperty( + "org.apache.tomcat.util.http.ServerCookie.ALWAYS_ADD_EXPIRES"); + if (alwaysAddExpires == null) { + ALWAYS_ADD_EXPIRES = !STRICT_SERVLET_COMPLIANCE; + } else { + ALWAYS_ADD_EXPIRES = + Boolean.valueOf(alwaysAddExpires).booleanValue(); + } + + String fwdSlashIsSeparator = System.getProperty( + "org.apache.tomcat.util.http.ServerCookie.FWD_SLASH_IS_SEPARATOR"); + if (fwdSlashIsSeparator == null) { + FWD_SLASH_IS_SEPARATOR = STRICT_SERVLET_COMPLIANCE; + } else { + FWD_SLASH_IS_SEPARATOR = + Boolean.valueOf(fwdSlashIsSeparator).booleanValue(); + } + + /* + Excluding the '/' char by default violates the RFC, but + it looks like a lot of people put '/' + in unquoted values: '/': ; //47 + '\t':9 ' ':32 '\"':34 '(':40 ')':41 ',':44 ':':58 ';':59 '<':60 + '=':61 '>':62 '?':63 '@':64 '[':91 '\\':92 ']':93 '{':123 '}':125 + */ + if (CookieSupport.FWD_SLASH_IS_SEPARATOR) { + HTTP_SEPARATORS = new char[] { '\t', ' ', '\"', '(', ')', ',', '/', + ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}' }; + } else { + HTTP_SEPARATORS = new char[] { '\t', ' ', '\"', '(', ')', ',', + ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}' }; + } + for (int i = 0; i < 128; i++) { + V0_SEPARATOR_FLAGS[i] = false; + HTTP_SEPARATOR_FLAGS[i] = false; + } + for (int i = 0; i < V0_SEPARATORS.length; i++) { + V0_SEPARATOR_FLAGS[V0_SEPARATORS[i]] = true; + } + for (int i = 0; i < HTTP_SEPARATORS.length; i++) { + HTTP_SEPARATOR_FLAGS[HTTP_SEPARATORS[i]] = true; + } + + } + + // ----------------------------------------------------------------- Methods + + /** + * Returns true if the byte is a separator as defined by V0 of the cookie + * spec. + */ + public static final boolean isV0Separator(final char c) { + if (c < 0x20 || c >= 0x7f) { + if (c != 0x09) { + throw new IllegalArgumentException( + "Control character in cookie value or attribute."); + } + } + + return V0_SEPARATOR_FLAGS[c]; + } + + public static boolean isV0Token(String value) { + if( value==null) return false; + + int i = 0; + int len = value.length(); + + if (alreadyQuoted(value)) { + i++; + len--; + } + + for (; i < len; i++) { + char c = value.charAt(i); + + if (isV0Separator(c)) + return true; + } + return false; + } + + /** + * Returns true if the byte is a separator as defined by V1 of the cookie + * spec, RFC2109. + * @throws IllegalArgumentException if a control character was supplied as + * input + */ + public static final boolean isHttpSeparator(final char c) { + if (c < 0x20 || c >= 0x7f) { + if (c != 0x09) { + throw new IllegalArgumentException( + "Control character in cookie value or attribute."); + } + } + + return HTTP_SEPARATOR_FLAGS[c]; + } + + public static boolean isHttpToken(String value) { + if( value==null) return false; + + int i = 0; + int len = value.length(); + + if (alreadyQuoted(value)) { + i++; + len--; + } + + for (; i < len; i++) { + char c = value.charAt(i); + + if (isHttpSeparator(c)) + return true; + } + return false; + } + + public static boolean containsCTL(String value) { + 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 alreadyQuoted (String value) { + if (value==null || value.length() < 2) return false; + return (value.charAt(0)=='\"' && value.charAt(value.length()-1)=='\"'); + } + + + + // ------------------------------------------------------------- Constructor + private CookieSupport() { + // Utility class. Don't allow instances to be created. + } +} Propchange: tomcat/trunk/java/org/apache/tomcat/util/http/CookieSupport.java ------------------------------------------------------------------------------ svn:eol-style = native Modified: tomcat/trunk/java/org/apache/tomcat/util/http/Cookies.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/http/Cookies.java?rev=836113&r1=836112&r2=836113&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/http/Cookies.java (original) +++ tomcat/trunk/java/org/apache/tomcat/util/http/Cookies.java Sat Nov 14 03:47:48 2009 @@ -19,7 +19,6 @@ import java.io.PrintWriter; import java.io.StringWriter; -import java.util.StringTokenizer; import org.apache.tomcat.util.buf.ByteChunk; import org.apache.tomcat.util.buf.MessageBytes; @@ -47,72 +46,6 @@ MimeHeaders headers; /** - * If set to true, we parse cookies strictly according to the servlet, - * cookie and HTTP specs by default. - */ - public static final boolean STRICT_SERVLET_COMPLIANCE; - - /** - * If true, cookie values are allowed to contain an equals character without - * being quoted. - */ - public static final boolean ALLOW_EQUALS_IN_VALUE; - - /** - * If set to true, the <code>/</code> character will be treated as a - * separator. Default is usually false. If STRICT_SERVLET_COMPLIANCE==true - * then default is true. Explicitly setting always takes priority. - */ - public static final boolean FWD_SLASH_IS_SEPARATOR; - - /* - List of Separator Characters (see isSeparator()) - */ - public static final char SEPARATORS[]; - - protected static final boolean separators[] = new boolean[128]; - static { - STRICT_SERVLET_COMPLIANCE = Boolean.valueOf(System.getProperty( - "org.apache.catalina.STRICT_SERVLET_COMPLIANCE", - "false")).booleanValue(); - - ALLOW_EQUALS_IN_VALUE = Boolean.valueOf(System.getProperty( - "org.apache.tomcat.util.http.ServerCookie.ALLOW_EQUALS_IN_VALUE", - "false")).booleanValue(); - - String fwdSlashIsSeparator = System.getProperty( - "org.apache.tomcat.util.http.ServerCookie.FWD_SLASH_IS_SEPARATOR"); - if (fwdSlashIsSeparator == null) { - FWD_SLASH_IS_SEPARATOR = STRICT_SERVLET_COMPLIANCE; - } else { - FWD_SLASH_IS_SEPARATOR = - Boolean.valueOf(fwdSlashIsSeparator).booleanValue(); - } - - /* - Excluding the '/' char by default violates the RFC, but - it looks like a lot of people put '/' - in unquoted values: '/': ; //47 - '\t':9 ' ':32 '\"':34 '(':40 ')':41 ',':44 ':':58 ';':59 '<':60 - '=':61 '>':62 '?':63 '@':64 '[':91 '\\':92 ']':93 '{':123 '}':125 - */ - if (FWD_SLASH_IS_SEPARATOR) { - SEPARATORS = new char[] { '\t', ' ', '\"', '(', ')', ',', '/', - ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}' }; - } else { - SEPARATORS = new char[] { '\t', ' ', '\"', '(', ')', ',', - ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}' }; - } - - 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 * the information from headers. * @@ -124,29 +57,6 @@ } /** - * Construct a new uninitialized cookie collection. - * Use {...@link #setHeaders} to initialize. - */ - // [seguin] added so that an empty Cookies object could be - // created, have headers set, then recycled. - public Cookies() { - } - - /** - * Set the headers from which cookies will be pulled. - * This has the side effect of recycling the object. - * - * @param headers Cookies are lazy-evaluated and will extract the - * information from the provided headers. - */ - // [seguin] added so that an empty Cookies object could be - // created, have headers set, then recycled. - public void setHeaders(MimeHeaders headers) { - recycle(); - this.headers=headers; - } - - /** * Recycle. */ public void recycle() { @@ -196,7 +106,7 @@ * most of the time an existing ServerCookie object is returned. * The caller can set the name/value and attributes for the cookie */ - public ServerCookie addCookie() { + private ServerCookie addCookie() { if( cookieCount >= scookies.length ) { ServerCookie scookiesTmp[]=new ServerCookie[2*cookieCount]; System.arraycopy( scookies, 0, scookiesTmp, 0, cookieCount); @@ -235,24 +145,24 @@ } // Uncomment to test the new parsing code - if( cookieValue.getType() == MessageBytes.T_BYTES ) { - if(log.isDebugEnabled()) - log.debug("Cookies: Parsing b[]: " + cookieValue.toString()); - ByteChunk bc=cookieValue.getByteChunk(); - processCookieHeader( bc.getBytes(), - bc.getOffset(), - bc.getLength()); - } else { - if(log.isDebugEnabled()) - log.debug("Cookies: Parsing S: " + cookieValue.toString()); - processCookieHeader( cookieValue.toString() ); - } + if( cookieValue.getType() != MessageBytes.T_BYTES ) { + Exception e = new Exception(); + log.warn("Cookies: Parsing cookie as String. Expected bytes.", + e); + cookieValue.toBytes(); + } + if(log.isDebugEnabled()) + log.debug("Cookies: Parsing b[]: " + cookieValue.toString()); + ByteChunk bc=cookieValue.getByteChunk(); + processCookieHeader( bc.getBytes(), + bc.getOffset(), + bc.getLength()); pos++;// search from the next position } } // XXX will be refactored soon! - public static boolean equals( String s, byte b[], int start, int end) { + private static boolean equals( String s, byte b[], int start, int end) { int blen = end-start; if (b == null || blen != s.length()) { return false; @@ -267,87 +177,12 @@ } - // --------------------------------------------------------- - // -------------------- DEPRECATED, OLD -------------------- - - private void processCookieHeader( String cookieString ) - { - if(log.isDebugEnabled()) - log.debug("Cookies: Parsing cookie header " + cookieString); - // normal cookie, with a string value. - // This is the original code, un-optimized - it shouldn't - // happen in normal case - - StringTokenizer tok = new StringTokenizer(cookieString, - ";", false); - while (tok.hasMoreTokens()) { - String token = tok.nextToken(); - int i = token.indexOf("="); - if (i > -1) { - - // XXX - // the trims here are a *hack* -- this should - // be more properly fixed to be spec compliant - - String name = token.substring(0, i).trim(); - String value = token.substring(i+1, token.length()).trim(); - // RFC 2109 and bug - value=stripQuote( value ); - ServerCookie cookie = addCookie(); - - cookie.getName().setString(name); - cookie.getValue().setString(value); - if(log.isDebugEnabled()) - log.debug("Cookies: Add cookie " + name + "=" + value); - } else { - // we have a bad cookie.... just let it go - } - } - } - - /** - * - * Strips quotes from the start and end of the cookie string - * This conforms to RFC 2965 - * - * @param value a <code>String</code> specifying the cookie - * value (possibly quoted). - * - * @see #setValue - * - */ - private static String stripQuote( String value ) - { - // log("Strip quote from " + value ); - if (value.startsWith("\"") && value.endsWith("\"")) { - try { - return value.substring(1,value.length()-1); - } catch (Exception ex) { - } - } - return value; - } - - /** - * 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. - * JVK - */ - 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 * JVK */ - public static final boolean isWhiteSpace(final byte c) { + private 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) @@ -370,12 +205,39 @@ } /** + * Unescapes any double quotes in the given cookie value. + * + * @param bc The cookie value to modify + */ + private 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); + } + + /** * 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){ + protected final void processCookieHeader(byte bytes[], int off, int len){ if( len<=0 || bytes==null ) return; int end=off+len; int pos=off; @@ -394,7 +256,10 @@ // Skip whitespace and non-token characters (separators) while (pos < end && - (isSeparator(bytes[pos]) || isWhiteSpace(bytes[pos]))) + (CookieSupport.isHttpSeparator((char) bytes[pos]) && + !CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 || + CookieSupport.isV0Separator((char) bytes[pos]) || + isWhiteSpace(bytes[pos]))) {pos++; } if (pos >= end) @@ -406,9 +271,9 @@ pos++; } - // Get the cookie name. This must be a token + // Get the cookie/attribute name. This must be a token valueEnd = valueStart = nameStart = pos; - pos = nameEnd = getTokenEndPosition(bytes,pos,end); + pos = nameEnd = getTokenEndPosition(bytes,pos,end,version,true); // Skip whitespace while (pos < end && isWhiteSpace(bytes[pos])) {pos++; } @@ -455,19 +320,23 @@ // The position is OK (On a delimiter) break; default: - if (!isSeparator(bytes[pos])) { + if (version == 0 && + !CookieSupport.isV0Separator((char)bytes[pos]) && + CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 || + !CookieSupport.isHttpSeparator((char)bytes[pos])) { // Token valueStart=pos; // getToken returns the position at the delimeter // or other non-token character - valueEnd=getTokenEndPosition(bytes, valueStart, end); + valueEnd=getTokenEndPosition(bytes, valueStart, end, + version, false); // 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.info("Cookies: Invalid cookie." + + log.info("Cookies: Invalid cookie. " + "Value not a token or quoted value"); while (pos < end && bytes[pos] != ';' && bytes[pos] != ',') @@ -503,23 +372,6 @@ 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) { @@ -557,12 +409,11 @@ continue; } - + // v2 cookie attributes - skip them if (equals( "Port", bytes, nameStart, nameEnd)) { - // sc.getPort is not currently implemented. - // sc.getPort().setBytes( bytes, - // valueStart, - // valueEnd-valueStart ); + continue; + } + if (equals( "CommentURL", bytes, nameStart, nameEnd)) { continue; } @@ -580,8 +431,7 @@ valueEnd-valueStart); if (isQuoted) { // We know this is a byte value so this is safe - ServerCookie.unescapeDoubleQuotes( - sc.getValue().getByteChunk()); + unescapeDoubleQuotes(sc.getValue().getByteChunk()); } } else { // Name Only @@ -597,11 +447,17 @@ * token, with no separator characters in between. * JVK */ - public static final int getTokenEndPosition(byte bytes[], int off, int end){ + private static final int getTokenEndPosition(byte bytes[], int off, int end, + int version, boolean isName){ int pos = off; while (pos < end && - (!isSeparator(bytes[pos]) || - bytes[pos]=='=' && ALLOW_EQUALS_IN_VALUE)) { + (!CookieSupport.isHttpSeparator((char)bytes[pos]) || + version == 0 && + CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 && + bytes[pos] != '=' && + !CookieSupport.isV0Separator((char)bytes[pos]) || + !isName && bytes[pos] == '=' && + CookieSupport.ALLOW_EQUALS_IN_VALUE)) { pos++; } @@ -615,7 +471,7 @@ * 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){ + private static final int getQuotedValueEndPosition(byte bytes[], int off, int end){ int pos = off; while (pos < end) { if (bytes[pos] == '"') { Modified: tomcat/trunk/java/org/apache/tomcat/util/http/ServerCookie.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/http/ServerCookie.java?rev=836113&r1=836112&r2=836113&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/http/ServerCookie.java (original) +++ tomcat/trunk/java/org/apache/tomcat/util/http/ServerCookie.java Sat Nov 14 03:47:48 2009 @@ -25,7 +25,6 @@ import java.util.Locale; import java.util.TimeZone; -import org.apache.tomcat.util.buf.ByteChunk; import org.apache.tomcat.util.buf.MessageBytes; @@ -40,6 +39,7 @@ */ public class ServerCookie implements Serializable { + private static final long serialVersionUID = 1L; // Version 0 (Netscape) attributes private MessageBytes name=MessageBytes.newInstance(); @@ -69,69 +69,21 @@ }; private static final String ancientDate; - /** - * If set to true, we parse cookies strictly according to the servlet, - * cookie and HTTP specs by default. - */ - public static final boolean STRICT_SERVLET_COMPLIANCE; - - /** - * If set to false, we don't use the IE6/7 Max-Age/Expires work around. - * Default is usually true. If STRICT_SERVLET_COMPLIANCE==true then default - * is false. Explicitly setting always takes priority. - */ - public static final boolean ALWAYS_ADD_EXPIRES; - - /** - * If set to true, the <code>/</code> character will be treated as a - * separator. Default is usually false. If STRICT_SERVLET_COMPLIANCE==true - * then default is true. Explicitly setting always takes priority. - */ - public static final boolean FWD_SLASH_IS_SEPARATOR; - - static { ancientDate = OLD_COOKIE_FORMAT.get().format(new Date(10000)); - - STRICT_SERVLET_COMPLIANCE = Boolean.valueOf(System.getProperty( - "org.apache.catalina.STRICT_SERVLET_COMPLIANCE", - "false")).booleanValue(); - - - String alwaysAddExpires = System.getProperty( - "org.apache.tomcat.util.http.ServerCookie.ALWAYS_ADD_EXPIRES"); - if (alwaysAddExpires == null) { - ALWAYS_ADD_EXPIRES = !STRICT_SERVLET_COMPLIANCE; - } else { - ALWAYS_ADD_EXPIRES = - Boolean.valueOf(alwaysAddExpires).booleanValue(); - } - - String fwdSlashIsSeparator = System.getProperty( - "org.apache.tomcat.util.http.ServerCookie.FWD_SLASH_IS_SEPARATOR"); - if (fwdSlashIsSeparator == null) { - FWD_SLASH_IS_SEPARATOR = STRICT_SERVLET_COMPLIANCE; - } else { - FWD_SLASH_IS_SEPARATOR = - Boolean.valueOf(fwdSlashIsSeparator).booleanValue(); - } - - if (FWD_SLASH_IS_SEPARATOR) { - tspecials2 = "()<>@,;:\\\"/[]?={} \t"; - } else { - tspecials2 = "()<>@,;:\\\"[]?={} \t"; - } } - // Note: Servlet Spec =< 2.5 only refers to Netscape and RFC2109, + // Note: Servlet Spec =< 3.0 only refers to Netscape and RFC2109, // not RFC2965 - // Version 1 (RFC2965) attributes - // TODO Add support for CommentURL + // Version 2 (RFC2965) attributes that would need to be added to support + // v2 cookies + // CommentURL // Discard - implied by maxAge <0 - // TODO Add support for Port + // Port public ServerCookie() { + // NOOP } public void recycle() { @@ -199,98 +151,9 @@ + getVersion() + " " + getPath() + " " + getDomain(); } - private static final String tspecials = ",; "; - private static final String tspecials2; - - /* - * Tests a string and returns true if the string counts as a - * reserved token in the Java language. - * - * @param value the <code>String</code> to be tested - * - * @return <code>true</code> if the <code>String</code> is a reserved - * token; <code>false</code> 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 (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 tokens = (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 (tokens.indexOf(c) != -1) - return false; - } - return true; - } - // -------------------- Cookie parsing tools - /** - * Return the header name to set the cookie, based on cookie version. - */ - public String getCookieHeaderName() { - return getCookieHeaderName(version); - } - - /** - * Return the header name to set the cookie, based on cookie version. - */ - public static String getCookieHeaderName(int version) { - // TODO Re-enable logging when RFC2965 is implemented - // log( (version==1) ? "Set-Cookie2" : "Set-Cookie"); - if (version == 1) { - // XXX RFC2965 not referenced in Servlet Spec - // Set-Cookie2 is not supported by Netscape 4, 6, IE 3, 5 - // Set-Cookie2 is supported by Lynx and Opera - // Need to check on later IE and FF releases but for now... - // RFC2109 - return "Set-Cookie"; - // return "Set-Cookie2"; - } else { - // Old Netscape - return "Set-Cookie"; - } - } - - // TODO RFC2965 fields also need to be passed public static void appendCookieValue( StringBuffer headerBuf, int version, String name, @@ -308,41 +171,81 @@ buf.append("="); // Servlet implementation does not check anything else - version = maybeQuote2(version, buf, value, true); + /* + * The spec allows some latitude on when to send the version attribute + * with a Set-Cookie header. To be nice to clients, we'll make sure the + * version attribute is first. That means checking the various things + * that can cause us to switch to a v1 cookie first. + * + * Note that by checking for tokens we will also throw an exception if a + * control character is encountered. + */ + // Start by using the version we were asked for + int newVersion = version; - // Spec team clarified setting comment on a v0 cookie switches it to v1 - if (version == 0 && comment != null) { - version = 1; + // If it is v0, check if we need to switch + if (newVersion == 0 && + (!CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 && + CookieSupport.isHttpToken(value) || + CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 && + CookieSupport.isV0Token(value))) { + // HTTP token in value - need to use v1 + newVersion = 1; } - + + if (newVersion == 0 && comment != null) { + // Using a comment makes it a v1 cookie + newVersion = 1; + } + + if (newVersion == 0 && + (!CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 && + CookieSupport.isHttpToken(path) || + CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 && + CookieSupport.isV0Token(path))) { + // HTTP token in path - need to use v1 + newVersion = 1; + } + + if (newVersion == 0 && + (!CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 && + CookieSupport.isHttpToken(domain) || + CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 && + CookieSupport.isV0Token(domain))) { + // HTTP token in domain - need to use v1 + newVersion = 1; + } + + // Now build the cookie header + // Value + maybeQuote(buf, value); // Add version 1 specific information - if (version == 1) { + if (newVersion == 1) { // Version=1 ... required buf.append ("; Version=1"); // Comment=comment if ( comment!=null ) { buf.append ("; Comment="); - maybeQuote2(version, buf, comment); + maybeQuote(buf, comment); } } // Add domain information, if present if (domain!=null) { buf.append("; Domain="); - maybeQuote2(version, buf, domain); + maybeQuote(buf, domain); } // Max-Age=secs ... or use old "Expires" format - // TODO RFC2965 Discard if (maxAge >= 0) { - if (version > 0) { + if (newVersion > 0) { buf.append ("; Max-Age="); buf.append (maxAge); } // IE6, IE7 and possibly other browsers don't understand Max-Age. // They do understand Expires, even with V1 cookies! - if (version == 0 || ALWAYS_ADD_EXPIRES) { + if (newVersion == 0 || CookieSupport.ALWAYS_ADD_EXPIRES) { // Wdy, DD-Mon-YY HH:MM:SS GMT ( Expires Netscape format ) buf.append ("; Expires="); // To expire immediately we need to set the time in past @@ -359,7 +262,7 @@ // Path=path if (path!=null) { buf.append ("; Path="); - maybeQuote2(version, buf, path); + maybeQuote(buf, path); } // Secure @@ -374,51 +277,28 @@ headerBuf.append(buf); } - public static boolean alreadyQuoted (String value) { - if (value==null || value.length()==0) return false; - return (value.charAt(0)=='\"' && value.charAt(value.length()-1)=='\"'); - } - /** - * Quotes values using rules that vary depending on Cookie version. - * @param version + * Quotes values if required. * @param buf * @param value */ - 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) { + private static void maybeQuote (StringBuffer buf, String value) { 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)) { + } else if (CookieSupport.alreadyQuoted(value)) { buf.append('"'); buf.append(escapeDoubleQuotes(value,1,value.length()-1)); buf.append('"'); - } else if (allowVersionSwitch && version==0 && !isToken2(value, literals)) { + } else if (CookieSupport.isHttpToken(value) && + !CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0 || + CookieSupport.isV0Token(value) && + CookieSupport.ALLOW_HTTP_SEPARATORS_IN_V0) { buf.append('"'); 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 { + } else { buf.append(value); } - return version; } @@ -453,31 +333,5 @@ 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); - } } Modified: tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesAllowEquals.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesAllowEquals.java?rev=836113&r1=836112&r2=836113&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesAllowEquals.java (original) +++ tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesAllowEquals.java Sat Nov 14 03:47:48 2009 @@ -68,7 +68,7 @@ disconnect(); reset(); tomcat.stop(); - assertTrue(response.contains(COOKIE_WITH_EQUALS)); + assertEquals(COOKIE_WITH_EQUALS, response); } @Override Added: tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesAllowHttpSeps.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesAllowHttpSeps.java?rev=836113&view=auto ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesAllowHttpSeps.java (added) +++ tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesAllowHttpSeps.java Sat Nov 14 03:47:48 2009 @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.util.http; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.SimpleHttpClient; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.catalina.startup.Tomcat; + +public class TestCookiesAllowHttpSeps extends TomcatBaseTest{ + + private static final String COOKIE_WITH_SEPS = "name=val(ue"; + + public void testWithHttpSep() throws Exception { + System.setProperty( + "org.apache.tomcat.util.http.ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0", + "true"); + + TestCookieHttpSepClient client = new TestCookieHttpSepClient(); + client.doRequest(); + } + + private class TestCookieHttpSepClient extends SimpleHttpClient { + + + private void doRequest() throws Exception { + Tomcat tomcat = getTomcatInstance(); + StandardContext root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "Simple", new SimpleServlet()); + root.addServletMapping("/test", "Simple"); + + tomcat.start(); + // Open connection + setPort(tomcat.getConnector().getPort()); + connect(); + + String[] request = new String[1]; + request[0] = + "GET /test HTTP/1.0" + CRLF + + "Cookie: " + COOKIE_WITH_SEPS + CRLF + CRLF; + setRequest(request); + processRequest(true); // blocks until response has been read + String response = getResponseBody(); + + // Close the connection + disconnect(); + reset(); + tomcat.stop(); + assertEquals(COOKIE_WITH_SEPS, response); + } + + @Override + public boolean isResponseBodyOK() { + return true; + } + + } + + + private static class SimpleServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + Cookie cookies[] = req.getCookies(); + for (Cookie cookie : cookies) { + resp.getWriter().write(cookie.getName() + "=" + + cookie.getValue() + "\n"); + } + resp.flushBuffer(); + } + + } + +} Propchange: tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesAllowHttpSeps.java ------------------------------------------------------------------------------ svn:eol-style = native Modified: tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesDefaultSysProps.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesDefaultSysProps.java?rev=836113&r1=836112&r2=836113&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesDefaultSysProps.java (original) +++ tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesDefaultSysProps.java Sat Nov 14 03:47:48 2009 @@ -59,9 +59,7 @@ getUrl("http://localhost:" + getPort() + "/switch", res, headers); List<String> cookieHeaders = headers.get("Set-Cookie"); for (String cookieHeader : cookieHeaders) { - if (cookieHeader.contains("name=")) { - assertTrue(cookieHeader.contains("name=\"val?ue\";")); - } + assertEquals("name=\"val?ue\"; Version=1", cookieHeader); } } Modified: tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesDisallowEquals.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesDisallowEquals.java?rev=836113&r1=836112&r2=836113&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesDisallowEquals.java (original) +++ tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesDisallowEquals.java Sat Nov 14 03:47:48 2009 @@ -65,7 +65,7 @@ disconnect(); reset(); tomcat.stop(); - assertTrue(response.contains(COOKIE_TRUNCATED)); + assertEquals(COOKIE_TRUNCATED, response); } @Override Modified: tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesNoFwdStrictSysProps.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesNoFwdStrictSysProps.java?rev=836113&r1=836112&r2=836113&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesNoFwdStrictSysProps.java (original) +++ tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesNoFwdStrictSysProps.java Sat Nov 14 03:47:48 2009 @@ -51,7 +51,7 @@ res = getUrl("http://localhost:" + getPort() + "/invalidFwd"); assertEquals("Cookie name ok", res.toString()); res = getUrl("http://localhost:" + getPort() + "/invalidStrict"); - assertEquals("Cookie name fail", res.toString()); + assertEquals("Cookie name ok", res.toString()); // Will auto switch res = getUrl("http://localhost:" + getPort() + "/valid"); assertEquals("Cookie name ok", res.toString()); Modified: tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesStrictSysProps.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesStrictSysProps.java?rev=836113&r1=836112&r2=836113&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesStrictSysProps.java (original) +++ tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesStrictSysProps.java Sat Nov 14 03:47:48 2009 @@ -51,9 +51,9 @@ res = getUrl("http://localhost:" + getPort() + "/blank"); assertEquals("Cookie name fail", res.toString()); res = getUrl("http://localhost:" + getPort() + "/invalidFwd"); - assertEquals("Cookie name fail", res.toString()); + assertEquals("Cookie name ok", res.toString()); // Will auto-switch res = getUrl("http://localhost:" + getPort() + "/invalidStrict"); - assertEquals("Cookie name fail", res.toString()); + assertEquals("Cookie name ok", res.toString()); // Will auto-switch res = getUrl("http://localhost:" + getPort() + "/valid"); assertEquals("Cookie name ok", res.toString()); @@ -62,9 +62,7 @@ getUrl("http://localhost:" + getPort() + "/switch", res, headers); List<String> cookieHeaders = headers.get("Set-Cookie"); for (String cookieHeader : cookieHeaders) { - if (cookieHeader.contains("name=")) { - assertTrue(cookieHeader.contains("name=val?ue")); - } + assertEquals("name=\"val?ue\"; Version=1", cookieHeader); } } Modified: tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesSwitchSysProps.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesSwitchSysProps.java?rev=836113&r1=836112&r2=836113&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesSwitchSysProps.java (original) +++ tomcat/trunk/test/org/apache/tomcat/util/http/TestCookiesSwitchSysProps.java Sat Nov 14 03:47:48 2009 @@ -54,9 +54,9 @@ res = getUrl("http://localhost:" + getPort() + "/blank"); assertEquals("Cookie name fail", res.toString()); res = getUrl("http://localhost:" + getPort() + "/invalidFwd"); - assertEquals("Cookie name fail", res.toString()); + assertEquals("Cookie name ok", res.toString()); // Will auto-switch res = getUrl("http://localhost:" + getPort() + "/invalidStrict"); - assertEquals("Cookie name fail", res.toString()); + assertEquals("Cookie name ok", res.toString()); // Will auto-switch res = getUrl("http://localhost:" + getPort() + "/valid"); assertEquals("Cookie name ok", res.toString()); Modified: tomcat/trunk/webapps/docs/config/systemprops.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/config/systemprops.xml?rev=836113&r1=836112&r2=836113&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/config/systemprops.xml (original) +++ tomcat/trunk/webapps/docs/config/systemprops.xml Sat Nov 14 03:47:48 2009 @@ -248,8 +248,8 @@ </ul> </p> <p>Note that changing a number of the above defaults is likely to break - the majority of systems as a number of browsers are unable to correctly - handle the cookie headers that result from a strict adherence to the + the majority of systems as some browsers are unable to correctly handle + the cookie headers that result from a strict adherence to the specifications. Defaults, regardless of whether or not they have been changed by setting <code>org.apache.catalina.STRICT_SERVLET_COMPLIANCE</code> can always be @@ -277,6 +277,13 @@ </property> <property + name="org.apache.tomcat.util.http. ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0"> + <p>If this is <code>true</code> Tomcat will allow HTTP separators in + cookie names and values. If not specified, the default specification + compliant value of <code>false</code> will be used.</p> + </property> + + <property name="org.apache.tomcat.util.http. ServerCookie.ALWAYS_ADD_EXPIRES"> <p>If this is <code>true</code> Tomcat will always add an expires parameter to a SetCookie header even for cookies with version greater than @@ -294,7 +301,8 @@ be treated as a separator. Note that this character is frequently used in cookie path attributes and some browsers will fail to process a cookie if the path attribute is quoted as is required by a strict adherence to the - specifications. If not specified, the default value will be used. If + specifications. This is highly likely to break session tracking using + cookies. If not specified, the default value will be used. If <code>org.apache.catalina.STRICT_SERVLET_COMPLIANCE</code> is set to <code>true</code>, the default of this setting will be <code>true</code>, else the default value will be <code>false</code>.</p> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org