Author: markt Date: Wed Oct 1 15:13:54 2014 New Revision: 1628730 URL: http://svn.apache.org/r1628730 Log: Deprecate SetCookieSupport, moving the code into LegacyCookieProcessor and refactoring for per Context configuration as necessary.
Removed: tomcat/trunk/test/org/apache/tomcat/util/http/TestSetCookieSupportSeparatorsAllowed.java Modified: tomcat/trunk/java/org/apache/tomcat/util/http/LegacyCookieProcessor.java tomcat/trunk/java/org/apache/tomcat/util/http/SetCookieSupport.java tomcat/trunk/test/org/apache/tomcat/util/http/TestCookieProcessorGeneration.java Modified: tomcat/trunk/java/org/apache/tomcat/util/http/LegacyCookieProcessor.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/http/LegacyCookieProcessor.java?rev=1628730&r1=1628729&r2=1628730&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/http/LegacyCookieProcessor.java (original) +++ tomcat/trunk/java/org/apache/tomcat/util/http/LegacyCookieProcessor.java Wed Oct 1 15:13:54 2014 @@ -18,7 +18,15 @@ package org.apache.tomcat.util.http; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.SimpleDateFormat; import java.util.BitSet; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import javax.servlet.http.Cookie; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; @@ -54,10 +62,26 @@ public final class LegacyCookieProcessor '\t', ' ', '\"', '(', ')', ',', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '{', '}' }; + private static final String COOKIE_DATE_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss z"; + private static final ThreadLocal<DateFormat> COOKIE_DATE_FORMAT = + new ThreadLocal<DateFormat>() { + @Override + protected DateFormat initialValue() { + DateFormat df = + new SimpleDateFormat(COOKIE_DATE_PATTERN, Locale.US); + df.setTimeZone(TimeZone.getTimeZone("GMT")); + return df; + } + }; + + private static final String ANCIENT_DATE; + static { for (char c : V0_SEPARATORS) { V0_SEPARATOR_FLAGS.set(c); } + + ANCIENT_DATE = COOKIE_DATE_FORMAT.get().format(new Date(10000)); } @@ -74,7 +98,13 @@ public final class LegacyCookieProcessor // when deprecated code is removed private boolean presserveCookieHeader = CookieSupport.PRESERVE_COOKIE_HEADER; - private BitSet httpSeparatorFlags = new BitSet(128); + @SuppressWarnings("deprecation") // Default to STRICT_SERVLET_COMPLIANCE + // when deprecated code is removed + private boolean alwaysAddExpires = SetCookieSupport.ALWAYS_ADD_EXPIRES; + + private final BitSet httpSeparatorFlags = new BitSet(128); + + private final BitSet allowedWithoutQuotes = new BitSet(128); public LegacyCookieProcessor() { @@ -88,6 +118,34 @@ public final class LegacyCookieProcessor if (b) { httpSeparatorFlags.set('/'); } + + String separators; + if (getAllowHttpSepsInV0()) { + // comma, semi-colon and space as defined by netscape + separators = ",; "; + } else { + // separators as defined by RFC2616 + separators = "()<>@,;:\\\"/[]?={} \t"; + } + + // all CHARs except CTLs or separators are allowed without quoting + allowedWithoutQuotes.set(0x20, 0x7f); + for (char ch : separators.toCharArray()) { + allowedWithoutQuotes.clear(ch); + } + + /** + * Some browsers (e.g. IE6 and IE7) do not handle quoted Path values even + * when Version is set to 1. To allow for this, we support a property + * FWD_SLASH_IS_SEPARATOR which, when false, means a '/' character will not + * be treated as a separator, potentially avoiding quoting and the ensuing + * side effect of having the cookie upgraded to version 1. + * + * For now, we apply this rule globally rather than just to the Path attribute. + */ + if (!getAllowHttpSepsInV0() && !getForwardSlashIsSeparator()) { + allowedWithoutQuotes.set('/'); + } } @@ -118,6 +176,22 @@ public final class LegacyCookieProcessor public void setAllowHttpSepsInV0(boolean allowHttpSepsInV0) { this.allowHttpSepsInV0 = allowHttpSepsInV0; + // HTTP separators less comma, semicolon and space since the Netscape + // spec defines those as separators too. + // '/' is also treated as a special case + char[] seps = "()<>@:\\\"[]?={}\t".toCharArray(); + for (char sep : seps) { + if (allowHttpSepsInV0) { + allowedWithoutQuotes.set(sep); + } else { + allowedWithoutQuotes.clear(); + } + } + if (getForwardSlashIsSeparator() && !allowHttpSepsInV0) { + allowedWithoutQuotes.set('/'); + } else { + allowedWithoutQuotes.clear('/'); + } } @@ -142,6 +216,21 @@ public final class LegacyCookieProcessor } else { httpSeparatorFlags.clear('/'); } + if (forwardSlashIsSeparator && !getAllowHttpSepsInV0()) { + allowedWithoutQuotes.set('/'); + } else { + allowedWithoutQuotes.clear('/'); + } + } + + + public boolean getAlwaysAddExpires() { + return alwaysAddExpires; + } + + + public void setAlwaysAddExpires(boolean alwaysAddExpires) { + this.alwaysAddExpires = alwaysAddExpires; } @@ -193,8 +282,167 @@ public final class LegacyCookieProcessor @Override - public String generateHeader(javax.servlet.http.Cookie cookie) { - return SetCookieSupport.generateHeader(cookie); + public String generateHeader(Cookie cookie) { + /* + * 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. + */ + int version = cookie.getVersion(); + String value = cookie.getValue(); + String path = cookie.getPath(); + String domain = cookie.getDomain(); + String comment = cookie.getComment(); + + if (version == 0) { + // Check for the things that require a v1 cookie + if (needsQuotes(value) || comment != null || needsQuotes(path) || needsQuotes(domain)) { + version = 1; + } + } + + // Now build the cookie header + StringBuffer buf = new StringBuffer(); // can't use StringBuilder due to DateFormat + + // Just use the name supplied in the Cookie + buf.append(cookie.getName()); + buf.append("="); + + // Value + maybeQuote(buf, value); + + // Add version 1 specific information + if (version == 1) { + // Version=1 ... required + buf.append ("; Version=1"); + + // Comment=comment + if (comment != null) { + buf.append ("; Comment="); + maybeQuote(buf, comment); + } + } + + // Add domain information, if present + if (domain != null) { + buf.append("; Domain="); + maybeQuote(buf, domain); + } + + // Max-Age=secs ... or use old "Expires" format + int maxAge = cookie.getMaxAge(); + if (maxAge >= 0) { + if (version > 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 || getAlwaysAddExpires()) { + // 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 + if (maxAge == 0) { + buf.append( ANCIENT_DATE ); + } else { + COOKIE_DATE_FORMAT.get().format( + new Date(System.currentTimeMillis() + maxAge * 1000L), + buf, + new FieldPosition(0)); + } + } + } + + // Path=path + if (path!=null) { + buf.append ("; Path="); + maybeQuote(buf, path); + } + + // Secure + if (cookie.getSecure()) { + buf.append ("; Secure"); + } + + // HttpOnly + if (cookie.isHttpOnly()) { + buf.append("; HttpOnly"); + } + return buf.toString(); + } + + + private void maybeQuote(StringBuffer buf, String value) { + if (value == null || value.length() == 0) { + buf.append("\"\""); + } else if (alreadyQuoted(value)) { + buf.append('"'); + escapeDoubleQuotes(buf, value,1,value.length()-1); + buf.append('"'); + } else if (needsQuotes(value)) { + buf.append('"'); + escapeDoubleQuotes(buf, value,0,value.length()); + buf.append('"'); + } else { + buf.append(value); + } + } + + + private static void escapeDoubleQuotes(StringBuffer b, String s, int beginIndex, int endIndex) { + if (s.indexOf('"') == -1 && s.indexOf('\\') == -1) { + b.append(s); + return; + } + + for (int i = beginIndex; i < endIndex; i++) { + char c = s.charAt(i); + if (c == '\\' ) { + b.append('\\').append('\\'); + } else if (c == '"') { + b.append('\\').append('"'); + } else { + b.append(c); + } + } + } + + + private boolean needsQuotes(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 ((c < 0x20 && c != '\t') || c >= 0x7f) { + throw new IllegalArgumentException( + "Control character in cookie value or attribute."); + } + if (!allowedWithoutQuotes.get(c)) { + return true; + } + } + return false; + } + + + private static boolean alreadyQuoted (String value) { + return value.length() >= 2 && + value.charAt(0) == '\"' && + value.charAt(value.length() - 1) == '\"'; } Modified: tomcat/trunk/java/org/apache/tomcat/util/http/SetCookieSupport.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/tomcat/util/http/SetCookieSupport.java?rev=1628730&r1=1628729&r2=1628730&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/tomcat/util/http/SetCookieSupport.java (original) +++ tomcat/trunk/java/org/apache/tomcat/util/http/SetCookieSupport.java Wed Oct 1 15:13:54 2014 @@ -16,26 +16,21 @@ */ package org.apache.tomcat.util.http; -import java.text.DateFormat; -import java.text.FieldPosition; -import java.text.SimpleDateFormat; -import java.util.BitSet; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; - import javax.servlet.http.Cookie; /** * Support class for generating Set-Cookie header values. + * + * @deprecated Will be removed in Tomcat 9. */ +@Deprecated public class SetCookieSupport { /** * 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. */ - private static final boolean ALWAYS_ADD_EXPIRES; + static final boolean ALWAYS_ADD_EXPIRES; static { String alwaysAddExpires = System.getProperty( "org.apache.tomcat.util.http.ServerCookie.ALWAYS_ADD_EXPIRES"); @@ -46,225 +41,9 @@ public class SetCookieSupport { } } - private static final BitSet ALLOWED_WITHOUT_QUOTES; - static { - boolean allowSeparatorsInV0 = Boolean.getBoolean( - "org.apache.tomcat.util.http.ServerCookie.ALLOW_HTTP_SEPARATORS_IN_V0"); - String separators; - if (allowSeparatorsInV0) { - // comma, semi-colon and space as defined by netscape - separators = ",; "; - } else { - // separators as defined by RFC2616 - separators = "()<>@,;:\\\"/[]?={} \t"; - } - - // all CHARs except CTLs or separators are allowed without quoting - ALLOWED_WITHOUT_QUOTES = new BitSet(128); - ALLOWED_WITHOUT_QUOTES.set(0x20, 0x7f); - for (char ch : separators.toCharArray()) { - ALLOWED_WITHOUT_QUOTES.clear(ch); - } - - /** - * Some browsers (e.g. IE6 and IE7) do not handle quoted Path values even - * when Version is set to 1. To allow for this, we support a property - * FWD_SLASH_IS_SEPARATOR which, when false, means a '/' character will not - * be treated as a separator, potentially avoiding quoting and the ensuing - * side effect of having the cookie upgraded to version 1. - * - * For now, we apply this rule globally rather than just to the Path attribute. - */ - if (!allowSeparatorsInV0) { - boolean allowSlash; - String prop = System.getProperty( - "org.apache.tomcat.util.http.ServerCookie.FWD_SLASH_IS_SEPARATOR"); - if (prop != null) { - allowSlash = !Boolean.parseBoolean(prop); - } else { - allowSlash = !Boolean.getBoolean("org.apache.catalina.STRICT_SERVLET_COMPLIANCE"); - } - if (allowSlash) { - ALLOWED_WITHOUT_QUOTES.set('/'); - } - } - } - - // Other fields - private static final String OLD_COOKIE_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss z"; - private static final ThreadLocal<DateFormat> OLD_COOKIE_FORMAT = - new ThreadLocal<DateFormat>() { - @Override - protected DateFormat initialValue() { - DateFormat df = - new SimpleDateFormat(OLD_COOKIE_PATTERN, Locale.US); - df.setTimeZone(TimeZone.getTimeZone("GMT")); - return df; - } - }; - private static final String ancientDate; - - static { - ancientDate = OLD_COOKIE_FORMAT.get().format(new Date(10000)); - } + private static final CookieProcessor cookieProcessor = new LegacyCookieProcessor(); public static String generateHeader(Cookie cookie) { - /* - * 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. - */ - int version = cookie.getVersion(); - String value = cookie.getValue(); - String path = cookie.getPath(); - String domain = cookie.getDomain(); - String comment = cookie.getComment(); - - if (version == 0) { - // Check for the things that require a v1 cookie - if (needsQuotes(value) || comment != null || needsQuotes(path) || needsQuotes(domain)) { - version = 1; - } - } - - // Now build the cookie header - StringBuffer buf = new StringBuffer(); // can't use StringBuilder due to DateFormat - - // Just use the name supplied in the Cookie - buf.append(cookie.getName()); - buf.append("="); - - // Value - maybeQuote(buf, value); - - // Add version 1 specific information - if (version == 1) { - // Version=1 ... required - buf.append ("; Version=1"); - - // Comment=comment - if (comment != null) { - buf.append ("; Comment="); - maybeQuote(buf, comment); - } - } - - // Add domain information, if present - if (domain != null) { - buf.append("; Domain="); - maybeQuote(buf, domain); - } - - // Max-Age=secs ... or use old "Expires" format - int maxAge = cookie.getMaxAge(); - if (maxAge >= 0) { - if (version > 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) { - // 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 - if (maxAge == 0) { - buf.append( ancientDate ); - } else { - OLD_COOKIE_FORMAT.get().format( - new Date(System.currentTimeMillis() + maxAge * 1000L), - buf, - new FieldPosition(0)); - } - } - } - - // Path=path - if (path!=null) { - buf.append ("; Path="); - maybeQuote(buf, path); - } - - // Secure - if (cookie.getSecure()) { - buf.append ("; Secure"); - } - - // HttpOnly - if (cookie.isHttpOnly()) { - buf.append("; HttpOnly"); - } - return buf.toString(); - } - - private static void maybeQuote(StringBuffer buf, String value) { - if (value == null || value.length() == 0) { - buf.append("\"\""); - } else if (alreadyQuoted(value)) { - buf.append('"'); - escapeDoubleQuotes(buf, value,1,value.length()-1); - buf.append('"'); - } else if (needsQuotes(value)) { - buf.append('"'); - escapeDoubleQuotes(buf, value,0,value.length()); - buf.append('"'); - } else { - buf.append(value); - } - } - - private static void escapeDoubleQuotes(StringBuffer b, String s, int beginIndex, int endIndex) { - if (s.indexOf('"') == -1 && s.indexOf('\\') == -1) { - b.append(s); - return; - } - - for (int i = beginIndex; i < endIndex; i++) { - char c = s.charAt(i); - if (c == '\\' ) { - b.append('\\').append('\\'); - } else if (c == '"') { - b.append('\\').append('"'); - } else { - b.append(c); - } - } - } - - private static boolean needsQuotes(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 ((c < 0x20 && c != '\t') || c >= 0x7f) { - throw new IllegalArgumentException( - "Control character in cookie value or attribute."); - } - if (!ALLOWED_WITHOUT_QUOTES.get(c)) { - return true; - } - } - return false; - } - - - private static boolean alreadyQuoted (String value) { - return value.length() >= 2 && - value.charAt(0) == '\"' && - value.charAt(value.length() - 1) == '\"'; + return cookieProcessor.generateHeader(cookie); } } Modified: tomcat/trunk/test/org/apache/tomcat/util/http/TestCookieProcessorGeneration.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/tomcat/util/http/TestCookieProcessorGeneration.java?rev=1628730&r1=1628729&r2=1628730&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/tomcat/util/http/TestCookieProcessorGeneration.java (original) +++ tomcat/trunk/test/org/apache/tomcat/util/http/TestCookieProcessorGeneration.java Wed Oct 1 15:13:54 2014 @@ -55,12 +55,16 @@ public class TestCookieProcessorGenerati @Test public void v0ValueContainsEquals() { - doTest(new Cookie("foo", "a=b"),"foo=\"a=b\"; Version=1", "foo=a=b"); + Cookie cookie = new Cookie("foo", "a=b"); + doTestDefaults(cookie, "foo=\"a=b\"; Version=1", "foo=a=b"); + doTestAllowSeparators(cookie, "foo=a=b", "foo=a=b"); } @Test public void v0ValueContainsQuote() { - doTest(new Cookie("foo", "a\"b"),"foo=\"a\\\"b\"; Version=1", null); + Cookie cookie = new Cookie("foo", "a\"b"); + doTestDefaults(cookie,"foo=\"a\\\"b\"; Version=1", null); + doTestAllowSeparators(cookie,"foo=a\"b", null); } @Test @@ -71,17 +75,23 @@ public class TestCookieProcessorGenerati @Test public void v0ValueContainsBackslash() { - doTest(new Cookie("foo", "a\\b"), "foo=\"a\\\\b\"; Version=1", null); + Cookie cookie = new Cookie("foo", "a\\b"); + doTestDefaults(cookie, "foo=\"a\\\\b\"; Version=1", null); + doTestAllowSeparators(cookie, "foo=a\\b", null); } @Test public void v0ValueContainsBackslashAtEnd() { - doTest(new Cookie("foo", "a\\"), "foo=\"a\\\\\"; Version=1", null); + Cookie cookie = new Cookie("foo", "a\\"); + doTestDefaults(cookie, "foo=\"a\\\\\"; Version=1", null); + doTestAllowSeparators(cookie, "foo=a\\", null); } @Test public void v0ValueContainsBackslashAndQuote() { - doTest(new Cookie("foo", "a\"b\\c"), "foo=\"a\\\"b\\\\c\"; Version=1", null); + Cookie cookie = new Cookie("foo", "a\"b\\c"); + doTestDefaults(cookie, "foo=\"a\\\"b\\\\c\"; Version=1", null); + doTestAllowSeparators(cookie, "foo=a\"b\\c", null); } @Test @@ -95,7 +105,6 @@ public class TestCookieProcessorGenerati public void v1NullValue() { Cookie cookie = new Cookie("foo", null); cookie.setVersion(1); - // should this throw an IAE? doTest(cookie, "foo=\"\"; Version=1", "foo="); } @@ -131,14 +140,16 @@ public class TestCookieProcessorGenerati public void v1ValueContainsEquals() { Cookie cookie = new Cookie("foo", "a=b"); cookie.setVersion(1); - doTest(cookie, "foo=\"a=b\"; Version=1", "foo=a=b"); + doTestDefaults(cookie, "foo=\"a=b\"; Version=1", "foo=a=b"); + doTestAllowSeparators(cookie, "foo=a=b; Version=1", "foo=a=b"); } @Test public void v1ValueContainsQuote() { Cookie cookie = new Cookie("foo", "a\"b"); cookie.setVersion(1); - doTest(cookie, "foo=\"a\\\"b\"; Version=1", null); + doTestDefaults(cookie, "foo=\"a\\\"b\"; Version=1", null); + doTestAllowSeparators(cookie, "foo=a\"b; Version=1", null); } @Test @@ -152,7 +163,8 @@ public class TestCookieProcessorGenerati public void v1ValueContainsBackslash() { Cookie cookie = new Cookie("foo", "a\\b"); cookie.setVersion(1); - doTest(cookie, "foo=\"a\\\\b\"; Version=1", null); + doTestDefaults(cookie, "foo=\"a\\\\b\"; Version=1", null); + doTestAllowSeparators(cookie, "foo=a\\b; Version=1", null); } @@ -160,7 +172,8 @@ public class TestCookieProcessorGenerati public void v1ValueContainsBackslashAndQuote() { Cookie cookie = new Cookie("foo", "a\"b\\c"); cookie.setVersion(1); - doTest(cookie, "foo=\"a\\\"b\\\\c\"; Version=1", null); + doTestDefaults(cookie, "foo=\"a\\\"b\\\\c\"; Version=1", null); + doTestAllowSeparators(cookie, "foo=a\"b\\c; Version=1", null); } private void doTest(Cookie cookie, String expected) { --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org