* Reimplemented CookieDecoder to understand quoted-strings

This commit is contained in:
Trustin Lee 2009-03-14 13:13:28 +00:00
parent 3e2bf2e163
commit eaca45eb8a
3 changed files with 185 additions and 147 deletions

View File

@ -35,7 +35,7 @@ final class CookieDateFormat extends SimpleDateFormat {
private static final long serialVersionUID = 1789486337887402640L;
CookieDateFormat() {
super("E, d-MMM-y H:m:s z");
super("E, d-MMM-y HH:mm:ss z");
setTimeZone(TimeZone.getTimeZone("GMT"));
}
}

View File

@ -23,9 +23,12 @@ package org.jboss.netty.handler.codec.http;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author The Netty Project (netty-dev@lists.jboss.org)
@ -35,55 +38,83 @@ import java.util.TreeSet;
*/
public class CookieDecoder {
private final static String SEMICOLON = ";";
private final static String EQUALS = "=";
private final static Pattern PATTERN =
Pattern.compile("(?:\\s|[;,])*\\$*([^;=]+)(?:=(?:[\"']((?:\\\\.|[^\"])*)[\"']|([^;,]*)))?\\s*(?:[;,]+|$)");
private final static String COMMA = ",";
private final String charset;
public CookieDecoder() {
this(QueryStringDecoder.DEFAULT_CHARSET);
}
public CookieDecoder(String charset) {
if (charset == null) {
throw new NullPointerException("charset");
}
this.charset = charset;
super();
}
public Set<Cookie> decode(String header) {
Set<Cookie> cookies = new TreeSet<Cookie>();
String[] split = header.split(SEMICOLON);
Matcher m = PATTERN.matcher(header);
List<String> names = new ArrayList<String>(8);
List<String> values = new ArrayList<String>(8);
int pos = 0;
int version = 0;
for (int i = 0; i < split.length; i++) {
boolean versionAtTheBeginning = false;
DefaultCookie theCookie;
String s = split[i];
String[] cookie = s.split(EQUALS, 2);
if (cookie != null && cookie.length == 2) {
String name = trimName(cookie[0]);
String value;
while (m.find(pos)) {
pos = m.end();
// $Version is the only attribute that can come before the
// actual cookie name-value pair.
if (!versionAtTheBeginning &&
name.equalsIgnoreCase(CookieHeaderNames.VERSION)) {
// Extract name and value pair from the match.
String name = m.group(1);
String value = m.group(3);
if (value == null) {
value = decodeValue(m.group(2));
}
// An exceptional case:
// 'Expires' attribute can contain a comma without surrounded with quotes.
if (name.equalsIgnoreCase(CookieHeaderNames.EXPIRES) &&
value.length() <= 3) {
// value contains comma, but not surrounded with quotes.
if (m.find(pos)) {
value = value + ", " + m.group(1);
pos = m.end();
} else {
continue;
}
}
names.add(name);
values.add(value);
}
if (names.isEmpty()) {
return Collections.emptySet();
}
int i;
// $Version is the only attribute that can appear before the actual
// cookie name-value pair.
if (names.get(0).equalsIgnoreCase(CookieHeaderNames.VERSION)) {
try {
version = Integer.parseInt(trimValue(cookie[1]));
version = Integer.parseInt(values.get(0));
} catch (NumberFormatException e) {
// Ignore.
}
versionAtTheBeginning = true;
continue;
i = 1;
} else {
i = 0;
}
// If it's not a version attribute, it's the name-value pair.
value = QueryStringDecoder.decodeComponent(trimValue(cookie[1]), charset);
theCookie = new DefaultCookie(name, value);
cookies.add(theCookie);
if (names.size() <= i) {
// There's a version attribute, but nothing more.
return Collections.emptySet();
}
Set<Cookie> cookies = new TreeSet<Cookie>();
for (; i < names.size(); i ++) {
String name = names.get(i);
String value = values.get(i);
if (value == null) {
value = "";
}
Cookie c = new DefaultCookie(name, value);
cookies.add(c);
boolean discard = false;
boolean secure = false;
String comment = null;
@ -92,41 +123,28 @@ public class CookieDecoder {
String path = null;
int maxAge = -1;
List<Integer> ports = new ArrayList<Integer>(2);
loop:
for (int j = i + 1; j < split.length; j++, i++) {
String[] val = split[j].split(EQUALS, 2);
if (val == null) {
continue;
}
switch (val.length) {
case 1:
if (CookieHeaderNames.DISCARD.equalsIgnoreCase(val[0])) {
for (int j = i + 1; j < names.size(); j++, i++) {
name = names.get(j);
value = values.get(j);
if (CookieHeaderNames.DISCARD.equalsIgnoreCase(name)) {
discard = true;
}
else if (CookieHeaderNames.SECURE.equalsIgnoreCase(val[0])) {
} else if (CookieHeaderNames.SECURE.equalsIgnoreCase(name)) {
secure = true;
}
break;
case 2:
name = trimName(val[0]);
value = trimValue(val[1]);
if (CookieHeaderNames.COMMENT.equalsIgnoreCase(name)) {
} else if (CookieHeaderNames.COMMENT.equalsIgnoreCase(name)) {
comment = value;
}
else if (CookieHeaderNames.COMMENTURL.equalsIgnoreCase(name)) {
value = trimValue(value);
} else if (CookieHeaderNames.COMMENTURL.equalsIgnoreCase(name)) {
commentURL = value;
}
else if (CookieHeaderNames.DOMAIN.equalsIgnoreCase(name)) {
} else if (CookieHeaderNames.DOMAIN.equalsIgnoreCase(name)) {
domain = value;
}
else if (CookieHeaderNames.PATH.equalsIgnoreCase(name)) {
} else if (CookieHeaderNames.PATH.equalsIgnoreCase(name)) {
path = value;
}
else if (CookieHeaderNames.EXPIRES.equalsIgnoreCase(name)) {
} else if (CookieHeaderNames.EXPIRES.equalsIgnoreCase(name)) {
try {
long maxAgeMillis =
new CookieDateFormat().parse(value).getTime() - System.currentTimeMillis();
new CookieDateFormat().parse(value).getTime() -
System.currentTimeMillis();
if (maxAgeMillis <= 0) {
maxAge = 0;
} else {
@ -134,17 +152,13 @@ public class CookieDecoder {
(maxAgeMillis % 1000 != 0? 1 : 0);
}
} catch (ParseException e) {
maxAge = 0;
// Ignore.
}
}
else if (CookieHeaderNames.MAX_AGE.equalsIgnoreCase(name)) {
} else if (CookieHeaderNames.MAX_AGE.equalsIgnoreCase(name)) {
maxAge = Integer.parseInt(value);
}
else if (CookieHeaderNames.VERSION.equalsIgnoreCase(name)) {
} else if (CookieHeaderNames.VERSION.equalsIgnoreCase(name)) {
version = Integer.parseInt(value);
}
else if (CookieHeaderNames.PORT.equalsIgnoreCase(name)) {
value = trimValue(value);
} else if (CookieHeaderNames.PORT.equalsIgnoreCase(name)) {
String[] portList = value.split(COMMA);
for (String s1: portList) {
try {
@ -154,49 +168,32 @@ public class CookieDecoder {
}
}
} else {
break loop;
}
break;
}
}
theCookie.setVersion(version);
theCookie.setMaxAge(maxAge);
theCookie.setPath(path);
theCookie.setDomain(domain);
theCookie.setSecure(secure);
c.setVersion(version);
c.setMaxAge(maxAge);
c.setPath(path);
c.setDomain(domain);
c.setSecure(secure);
if (version > 0) {
theCookie.setComment(comment);
c.setComment(comment);
}
if (version > 1) {
theCookie.setCommentUrl(commentURL);
theCookie.setPorts(ports);
theCookie.setDiscard(discard);
}
c.setCommentUrl(commentURL);
c.setPorts(ports);
c.setDiscard(discard);
}
}
return cookies;
}
private String trimName(String name) {
name = name.trim();
if (name.startsWith("$")) {
return name.substring(1);
} else {
return name;
}
}
private String trimValue(String value) {
value = value.trim();
if (value.length() >= 2) {
char firstChar = value.charAt(0);
char lastChar = value.charAt(value.length() - 1);
if ((firstChar == '"' || firstChar == '\'') &&
(lastChar == '"' || lastChar == '\'')) {
// Strip surrounding quotes.
value = value.substring(1, value.length() - 1);
}
}
private String decodeValue(String value) {
if (value == null) {
return value;
}
return value.replace("\\\"", "\"").replace("\\\\", "\\");
}
}

View File

@ -27,7 +27,6 @@ import java.util.Date;
import java.util.Iterator;
import java.util.Set;
import org.junit.Ignore;
import org.junit.Test;
/**
@ -61,7 +60,7 @@ public class CookieDecoderTest {
@Test
public void testDecodingSingleCookieV0ExtraParamsIgnored() {
String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;domain=.adomainsomewhere;secure;comment=this is a comment;version=0;commentURL=http://aurl.com;port=80,8080;discard;";
String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;domain=.adomainsomewhere;secure;comment=this is a comment;version=0;commentURL=http://aurl.com;port=\"80,8080\";discard;";
CookieDecoder cookieDecoder = new CookieDecoder();
Set<Cookie> cookies = cookieDecoder.decode(cookieString);
assertEquals(1, cookies.size());
@ -100,7 +99,7 @@ public class CookieDecoderTest {
@Test
public void testDecodingSingleCookieV1ExtraParamsIgnored() {
String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;domain=.adomainsomewhere;secure;comment=this is a comment;version=1;commentURL=http://aurl.com;port=80,8080;discard;";
String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;domain=.adomainsomewhere;secure;comment=this is a comment;version=1;commentURL=http://aurl.com;port='80,8080';discard;";
CookieDecoder cookieDecoder = new CookieDecoder();
Set<Cookie> cookies = cookieDecoder.decode(cookieString);
assertEquals(1, cookies.size());
@ -224,9 +223,7 @@ public class CookieDecoderTest {
}
@Test
@Ignore
public void testDecodingCommaSeparatedClientSideCookies() {
// FIXME Fix CookieDecoder to pass this test.
String source =
"$Version=\"1\"; session_id=\"1234\", " +
"$Version=\"1\"; session_id=\"1111\"; $Domain=\".cracker.edu\"";
@ -257,4 +254,48 @@ public class CookieDecoderTest {
assertTrue(c.getPorts().isEmpty());
assertEquals(-1, c.getMaxAge());
}
@Test
public void testDecodingQuotedCookie() {
String source =
"a=\"\"," +
"b=\"1\"," +
"c=\"\\\"1\\\"2\\\"\"," +
"d=\"1\\\"2\\\"3\"," +
"e=\"\\\"\\\"\"," +
"f=\"1\\\"\\\"2\"," +
"g=\"\\\\\"";
Set<Cookie> cookies = new CookieDecoder().decode(source);
Iterator<Cookie> it = cookies.iterator();
Cookie c;
c = it.next();
assertEquals("a", c.getName());
assertEquals("", c.getValue());
c = it.next();
assertEquals("b", c.getName());
assertEquals("1", c.getValue());
c = it.next();
assertEquals("c", c.getName());
assertEquals("\"1\"2\"", c.getValue());
c = it.next();
assertEquals("d", c.getName());
assertEquals("1\"2\"3", c.getValue());
c = it.next();
assertEquals("e", c.getName());
assertEquals("\"\"", c.getValue());
c = it.next();
assertEquals("f", c.getName());
assertEquals("1\"\"2", c.getValue());
c = it.next();
assertEquals("g", c.getName());
assertEquals("\\", c.getValue());
}
}