Issue #3655 simplifications of Cookie handling

Signed-off-by: Greg Wilkins <gregw@webtide.com>
This commit is contained in:
Greg Wilkins 2019-05-16 13:08:03 +02:00
parent 2de5456790
commit 26169491c9
5 changed files with 495 additions and 665 deletions

View File

@ -18,10 +18,17 @@
package org.eclipse.jetty.http;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.QuotedStringTokenizer;
public class HttpCookie
{
// TODO consider replacing this with java.net.HttpCookie
private static final String __COOKIE_DELIM="\",;\\ \t";
private static final String __01Jan1970_COOKIE = DateGenerator.formatCookieDate(0).trim();
private final String _name;
private final String _value;
private final String _comment;
@ -67,6 +74,29 @@ public class HttpCookie
_expiration = maxAge < 0 ? -1 : System.nanoTime() + TimeUnit.SECONDS.toNanos(maxAge);
}
enum State { START, NAME, VALUE, QUOTED}
public HttpCookie(String setCookie)
{
List<java.net.HttpCookie> cookies = java.net.HttpCookie.parse(setCookie);
if (cookies.size()!=1)
throw new IllegalStateException();
java.net.HttpCookie cookie = cookies.get(0);
_name = cookie.getName();
_value = cookie.getValue();
_domain = cookie.getDomain();
_path = cookie.getPath();
_maxAge = cookie.getMaxAge();
_httpOnly = cookie.isHttpOnly();
_secure = cookie.getSecure();
_comment = cookie.getComment();
_version = cookie.getVersion();
_expiration = _maxAge < 0 ? -1 : System.nanoTime() + TimeUnit.SECONDS.toNanos(_maxAge);
}
/**
* @return the cookie name
*/
@ -161,4 +191,201 @@ public class HttpCookie
builder.append(";$Path=").append(getPath());
return builder.toString();
}
private static void quoteOnlyOrAppend(StringBuilder buf, String s, boolean quote)
{
if (quote)
QuotedStringTokenizer.quoteOnly(buf,s);
else
buf.append(s);
}
/* ------------------------------------------------------------ */
/** Does a cookie value need to be quoted?
* @param s value string
* @return true if quoted;
* @throws IllegalArgumentException If there a control characters in the string
*/
private static boolean isQuoteNeededForCookie(String s)
{
if (s==null || s.length()==0)
return true;
if (QuotedStringTokenizer.isQuoted(s))
return false;
for (int i=0;i<s.length();i++)
{
char c = s.charAt(i);
if (__COOKIE_DELIM.indexOf(c)>=0)
return true;
if (c<0x20 || c>=0x7f)
throw new IllegalArgumentException("Illegal character in cookie value");
}
return false;
}
public String getSetCookie(CookieCompliance compliance)
{
switch(compliance)
{
case RFC2965:
return getRFC2965SetCookie();
case RFC6265:
return getRFC6265SetCookie();
default:
throw new IllegalStateException();
}
}
public String getRFC2965SetCookie()
{
// Check arguments
if (_name == null || _name.length() == 0)
throw new IllegalArgumentException("Bad cookie name");
// Format value and params
StringBuilder buf = new StringBuilder();
// Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting
boolean quote_name=isQuoteNeededForCookie(_name);
quoteOnlyOrAppend(buf,_name,quote_name);
buf.append('=');
// Append the value
boolean quote_value=isQuoteNeededForCookie(_value);
quoteOnlyOrAppend(buf,_value,quote_value);
// Look for domain and path fields and check if they need to be quoted
boolean has_domain = _domain!=null && _domain.length()>0;
boolean quote_domain = has_domain && isQuoteNeededForCookie(_domain);
boolean has_path = _path!=null && _path.length()>0;
boolean quote_path = has_path && isQuoteNeededForCookie(_path);
// Upgrade the version if we have a comment or we need to quote value/path/domain or if they were already quoted
int version = _version;
if (version==0 && ( _comment!=null || quote_name || quote_value || quote_domain || quote_path ||
QuotedStringTokenizer.isQuoted(_name) || QuotedStringTokenizer.isQuoted(_value) ||
QuotedStringTokenizer.isQuoted(_path) || QuotedStringTokenizer.isQuoted(_domain)))
version=1;
// Append version
if (version==1)
buf.append (";Version=1");
else if (version>1)
buf.append (";Version=").append(version);
// Append path
if (has_path)
{
buf.append(";Path=");
quoteOnlyOrAppend(buf,_path,quote_path);
}
// Append domain
if (has_domain)
{
buf.append(";Domain=");
quoteOnlyOrAppend(buf,_domain,quote_domain);
}
// Handle max-age and/or expires
if (_maxAge >= 0)
{
// Always use expires
// This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies
buf.append(";Expires=");
if (_maxAge == 0)
buf.append(__01Jan1970_COOKIE);
else
DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * _maxAge);
// for v1 cookies, also send max-age
if (version>=1)
{
buf.append(";Max-Age=");
buf.append(_maxAge);
}
}
// add the other fields
if (_secure)
buf.append(";Secure");
if (_httpOnly)
buf.append(";HttpOnly");
if (_comment != null)
{
buf.append(";Comment=");
quoteOnlyOrAppend(buf,_comment,isQuoteNeededForCookie(_comment));
}
return buf.toString();
}
public String getRFC6265SetCookie()
{
// Check arguments
if (_name == null || _name.length() == 0)
throw new IllegalArgumentException("Bad cookie name");
// Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting
// Per RFC6265, Cookie.name follows RFC2616 Section 2.2 token rules
Syntax.requireValidRFC2616Token(_name, "RFC6265 Cookie name");
// Ensure that Per RFC6265, Cookie.value follows syntax rules
Syntax.requireValidRFC6265CookieValue(_value);
// Format value and params
StringBuilder buf = new StringBuilder();
buf.append(_name).append('=').append(_value==null?"":_value);
// Append path
if (_path!=null && _path.length()>0)
buf.append("; Path=").append(_path);
// Append domain
if (_domain!=null && _domain.length()>0)
buf.append("; Domain=").append(_domain);
// Handle max-age and/or expires
if (_maxAge >= 0)
{
// Always use expires
// This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies
buf.append("; Expires=");
if (_maxAge == 0)
buf.append(__01Jan1970_COOKIE);
else
DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * _maxAge);
buf.append("; Max-Age=");
buf.append(_maxAge);
}
// add the other fields
if (_secure)
buf.append("; Secure");
if (_httpOnly)
buf.append("; HttpOnly");
return buf.toString();
}
public static class SetCookieHttpField extends HttpField
{
final HttpCookie _cookie;
public SetCookieHttpField(HttpCookie cookie, CookieCompliance compliance)
{
super(HttpHeader.SET_COOKIE, cookie.getSetCookie(compliance));
this._cookie = cookie;
}
public HttpCookie getHttpCookie()
{
return _cookie;
}
}
}

View File

@ -18,6 +18,14 @@
package org.eclipse.jetty.http;
/**
* Implements a quoted comma separated list parser
* in accordance with RFC7230.
* OWS is removed and quoted characters ignored for parsing.
* @see "https://tools.ietf.org/html/rfc7230#section-3.2.6"
* @see "https://tools.ietf.org/html/rfc7230#section-7"
*/
public abstract class QuotedCSVParser
{
private enum State { VALUE, PARAM_NAME, PARAM_VALUE}

View File

@ -0,0 +1,187 @@
//
// ========================================================================
// Copyright (c) 1995-2019 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.http;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class HttpCookieTest
{
@Test
public void testConstructFromSetCookie()
{
HttpCookie cookie = new HttpCookie("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly");
}
@Test
public void testSetRFC2965Cookie() throws Exception
{
HttpCookie httpCookie;
httpCookie = new HttpCookie("null", null, null, null, -1, false, false, null, -1);
assertEquals("null=",httpCookie.getRFC2965SetCookie());
httpCookie = new HttpCookie("minimal", "value", null, null, -1, false, false, null, -1);
assertEquals("minimal=value",httpCookie.getRFC2965SetCookie());
httpCookie = new HttpCookie("everything", "something", "domain", "path", 0, true, true, "noncomment", 0);
assertEquals("everything=something;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=noncomment",httpCookie.getRFC2965SetCookie());
httpCookie = new HttpCookie("everything", "value", "domain", "path", 0, true, true, "comment", 0);
assertEquals("everything=value;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",httpCookie.getRFC2965SetCookie());
httpCookie = new HttpCookie("ev erything", "va lue", "do main", "pa th", 1, true, true, "co mment", 1);
String setCookie=httpCookie.getRFC2965SetCookie();
assertThat(setCookie, Matchers.startsWith("\"ev erything\"=\"va lue\";Version=1;Path=\"pa th\";Domain=\"do main\";Expires="));
assertThat(setCookie,Matchers.endsWith(" GMT;Max-Age=1;Secure;HttpOnly;Comment=\"co mment\""));
httpCookie = new HttpCookie("name", "value", null, null, -1, false, false, null, 0);
setCookie=httpCookie.getRFC2965SetCookie();
assertEquals(-1,setCookie.indexOf("Version="));
httpCookie = new HttpCookie("name", "v a l u e", null, null, -1, false, false, null, 0);
setCookie=httpCookie.getRFC2965SetCookie();
httpCookie = new HttpCookie("json","{\"services\":[\"cwa\", \"aa\"]}", null, null, -1, false, false, null, -1);
assertEquals("json=\"{\\\"services\\\":[\\\"cwa\\\", \\\"aa\\\"]}\"",httpCookie.getRFC2965SetCookie());
httpCookie = new HttpCookie("name", "value%=", null, null, -1, false, false, null, 0);
setCookie=httpCookie.getRFC2965SetCookie();
assertEquals("name=value%=",setCookie);
}
@Test
public void testSetRFC6265Cookie() throws Exception
{
// HttpCookie(String name, String value, String domain, String path, long maxAge, boolean httpOnly, boolean secure, String comment, int version)
// httpCookie = new HttpCookie("name", "value", "domain", "path", maxAge, secure, httpOnly);
HttpCookie httpCookie;
httpCookie = new HttpCookie("null",null,null,null,-1,false,false, null, -1);
assertEquals("null=",httpCookie.getRFC6265SetCookie());
httpCookie = new HttpCookie("minimal","value",null,null,-1,false,false, null, -1);
assertEquals("minimal=value",httpCookie.getRFC6265SetCookie());
//test cookies with same name, domain and path
httpCookie = new HttpCookie("everything","something","domain","path",0,true,true, null, -1);
assertEquals("everything=something; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",httpCookie.getRFC6265SetCookie());
httpCookie = new HttpCookie("everything","value","domain","path",0,true,true, null, -1);
assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",httpCookie.getRFC6265SetCookie());
String badNameExamples[] = {
"\"name\"",
"name\t",
"na me",
"name\u0082",
"na\tme",
"na;me",
"{name}",
"[name]",
"\""
};
for (String badNameExample : badNameExamples)
{
try
{
httpCookie = new HttpCookie(badNameExample, "value", null, "/", 1, true, true, null, -1);
httpCookie.getRFC6265SetCookie();
fail(badNameExample);
}
catch (IllegalArgumentException ex)
{
// System.err.printf("%s: %s%n", ex.getClass().getSimpleName(), ex.getMessage());
assertThat("Testing bad name: [" + badNameExample + "]", ex.getMessage(),
allOf(containsString("RFC6265"), containsString("RFC2616")));
}
}
String badValueExamples[] = {
"va\tlue",
"\t",
"value\u0000",
"val\u0082ue",
"va lue",
"va;lue",
"\"value",
"value\"",
"val\\ue",
"val\"ue",
"\""
};
for (String badValueExample : badValueExamples)
{
try
{
httpCookie = new HttpCookie("name", badValueExample, null, "/", 1, true, true, null, -1);
httpCookie.getRFC6265SetCookie();
fail();
}
catch (IllegalArgumentException ex)
{
// System.err.printf("%s: %s%n", ex.getClass().getSimpleName(), ex.getMessage());
assertThat("Testing bad value [" + badValueExample + "]", ex.getMessage(), Matchers.containsString("RFC6265"));
}
}
String goodNameExamples[] = {
"name",
"n.a.m.e",
"na-me",
"+name",
"na*me",
"na$me",
"#name"
};
for (String goodNameExample : goodNameExamples)
{
httpCookie = new HttpCookie(goodNameExample, "value", null, "/", 1, true, true, null, -1);
// should not throw an exception
}
String goodValueExamples[] = {
"value",
"",
null,
"val=ue",
"val-ue",
"val/ue",
"v.a.l.u.e"
};
for (String goodValueExample : goodValueExamples)
{
httpCookie = new HttpCookie("name", goodValueExample, null, "/", 1, true, true, null, -1);
// should not throw an exception
}
}
}

View File

@ -40,6 +40,7 @@ import org.eclipse.jetty.http.CookieCompliance;
import org.eclipse.jetty.http.DateGenerator;
import org.eclipse.jetty.http.HttpContent;
import org.eclipse.jetty.http.HttpCookie;
import org.eclipse.jetty.http.HttpCookie.SetCookieHttpField;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator;
@ -69,12 +70,8 @@ import org.eclipse.jetty.util.log.Logger;
public class Response implements HttpServletResponse
{
private static final Logger LOG = Log.getLogger(Response.class);
private static final String __COOKIE_DELIM="\",;\\ \t";
private static final String __01Jan1970_COOKIE = DateGenerator.formatCookieDate(0).trim();
private static final int __MIN_BUFFER_SIZE = 1;
private static final HttpField __EXPIRES_01JAN1970 = new PreEncodedHttpField(HttpHeader.EXPIRES,DateGenerator.__01Jan1970);
// Cookie building buffer. Reduce garbage for cookie using applications
private static final ThreadLocal<StringBuilder> __cookieBuilder = ThreadLocal.withInitial(() -> new StringBuilder(128));
public enum OutputType
{
@ -168,30 +165,13 @@ public class Response implements HttpServletResponse
public void addCookie(HttpCookie cookie)
{
if (StringUtil.isBlank(cookie.getName()))
{
throw new IllegalArgumentException("Cookie.name cannot be blank/null");
}
if (getHttpChannel().getHttpConfiguration().getResponseCookieCompliance()==CookieCompliance.RFC2965)
addSetRFC2965Cookie(
cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
cookie.getMaxAge(),
cookie.getComment(),
cookie.isSecure(),
cookie.isHttpOnly(),
cookie.getVersion());
else
addSetRFC6265Cookie(
cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
cookie.getMaxAge(),
cookie.isSecure(),
cookie.isHttpOnly());
// add the set cookie
_fields.add(new SetCookieHttpField(cookie, getHttpChannel().getHttpConfiguration().getResponseCookieCompliance()));
// Expire responses with set-cookie headers so they do not get cached.
_fields.put(__EXPIRES_01JAN1970);
}
/**
@ -207,84 +187,34 @@ public class Response implements HttpServletResponse
if (field.getHeader() == HttpHeader.SET_COOKIE)
{
String old_set_cookie = field.getValue();
String name = cookie.getName();
if (!old_set_cookie.startsWith(name) || old_set_cookie.length()<= name.length() || old_set_cookie.charAt(name.length())!='=')
continue;
CookieCompliance compliance = getHttpChannel().getHttpConfiguration().getResponseCookieCompliance();
CookieCompliance responseCookieCompliance = getHttpChannel().getHttpConfiguration().getResponseCookieCompliance();
String expectedDomainSegment = "; Domain="; // default to RFC6265
if (responseCookieCompliance == CookieCompliance.RFC2965)
expectedDomainSegment = ";Domain=";
String domain = cookie.getDomain();
//noinspection Duplicates
if (domain!=null)
{
if (responseCookieCompliance == CookieCompliance.RFC2965)
{
StringBuilder buf = new StringBuilder();
buf.append(expectedDomainSegment);
quoteOnlyOrAppend(buf,domain,isQuoteNeededForCookie(domain));
domain = buf.toString();
}
else
{
domain = expectedDomainSegment+domain;
}
if (!old_set_cookie.contains(domain))
continue;
}
else if (old_set_cookie.contains(expectedDomainSegment))
continue;
String expectedPathSegment = "; Path="; // default to RFC6265
if (responseCookieCompliance == CookieCompliance.RFC2965)
expectedPathSegment = ";Path=";
String path = cookie.getPath();
//noinspection Duplicates
if (path!=null)
{
if (responseCookieCompliance == CookieCompliance.RFC2965)
{
StringBuilder buf = new StringBuilder();
buf.append(expectedPathSegment);
quoteOnlyOrAppend(buf,path,isQuoteNeededForCookie(path));
path = buf.toString();
}
else
{
path = expectedPathSegment+path;
}
if (!old_set_cookie.contains(path))
continue;
}
else if (old_set_cookie.contains(expectedPathSegment))
continue;
if (responseCookieCompliance == CookieCompliance.RFC2965)
i.set(new HttpField(HttpHeader.CONTENT_ENCODING.SET_COOKIE, newRFC2965SetCookie(
cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
cookie.getMaxAge(),
cookie.getComment(),
cookie.isSecure(),
cookie.isHttpOnly(),
cookie.getVersion())
));
HttpCookie oldCookie;
if (field instanceof SetCookieHttpField)
oldCookie = ((SetCookieHttpField)field).getHttpCookie();
else
i.set(new HttpField(HttpHeader.CONTENT_ENCODING.SET_COOKIE, newRFC6265SetCookie(
cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
cookie.getMaxAge(),
cookie.isSecure(),
cookie.isHttpOnly()
)));
oldCookie = new HttpCookie(field.getValue());
if (!cookie.getName().equals(oldCookie.getName()))
continue;
if (cookie.getDomain()==null)
{
if (oldCookie.getDomain() != null)
continue;
}
else if (!cookie.getDomain().equalsIgnoreCase(oldCookie.getDomain()))
continue;
if (cookie.getPath()==null)
{
if (oldCookie.getPath() != null)
continue;
}
else if (!cookie.getPath().equals(oldCookie.getPath()))
continue;
i.set(new SetCookieHttpField(cookie, compliance));
return;
}
}
@ -296,8 +226,11 @@ public class Response implements HttpServletResponse
@Override
public void addCookie(Cookie cookie)
{
if (StringUtil.isBlank(cookie.getName()))
throw new IllegalArgumentException("Cookie.name cannot be blank/null");
String comment = cookie.getComment();
boolean httpOnly = false;
boolean httpOnly = cookie.isHttpOnly();
if (comment != null)
{
@ -310,264 +243,19 @@ public class Response implements HttpServletResponse
comment = null;
}
}
if (StringUtil.isBlank(cookie.getName()))
{
throw new IllegalArgumentException("Cookie.name cannot be blank/null");
}
if (getHttpChannel().getHttpConfiguration().getResponseCookieCompliance()==CookieCompliance.RFC2965)
addSetRFC2965Cookie(cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
cookie.getMaxAge(),
comment,
cookie.getSecure(),
httpOnly || cookie.isHttpOnly(),
cookie.getVersion());
else
addSetRFC6265Cookie(cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
cookie.getMaxAge(),
cookie.getSecure(),
httpOnly || cookie.isHttpOnly());
addCookie(new HttpCookie(
cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
(long) cookie.getMaxAge(),
httpOnly,
cookie.getSecure(),
comment,
cookie.getVersion()));
}
/**
* Format a set cookie value by RFC6265
*
* @param name the name
* @param value the value
* @param domain the domain
* @param path the path
* @param maxAge the maximum age
* @param isSecure true if secure cookie
* @param isHttpOnly true if for http only
*/
public void addSetRFC6265Cookie(
final String name,
final String value,
final String domain,
final String path,
final long maxAge,
final boolean isSecure,
final boolean isHttpOnly)
{
String set_cookie = newRFC6265SetCookie(name, value, domain, path, maxAge, isSecure, isHttpOnly);
// add the set cookie
_fields.add(HttpHeader.SET_COOKIE, set_cookie);
// Expire responses with set-cookie headers so they do not get cached.
_fields.put(__EXPIRES_01JAN1970);
}
private String newRFC6265SetCookie(String name, String value, String domain, String path, long maxAge, boolean isSecure, boolean isHttpOnly)
{
// Check arguments
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Bad cookie name");
// Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting
// Per RFC6265, Cookie.name follows RFC2616 Section 2.2 token rules
Syntax.requireValidRFC2616Token(name, "RFC6265 Cookie name");
// Ensure that Per RFC6265, Cookie.value follows syntax rules
Syntax.requireValidRFC6265CookieValue(value);
// Format value and params
StringBuilder buf = __cookieBuilder.get();
buf.setLength(0);
buf.append(name).append('=').append(value==null?"":value);
// Append path
if (path!=null && path.length()>0)
buf.append("; Path=").append(path);
// Append domain
if (domain!=null && domain.length()>0)
buf.append("; Domain=").append(domain);
// Handle max-age and/or expires
if (maxAge >= 0)
{
// Always use expires
// This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies
buf.append("; Expires=");
if (maxAge == 0)
buf.append(__01Jan1970_COOKIE);
else
DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * maxAge);
buf.append("; Max-Age=");
buf.append(maxAge);
}
// add the other fields
if (isSecure)
buf.append("; Secure");
if (isHttpOnly)
buf.append("; HttpOnly");
return buf.toString();
}
/**
* Format a set cookie value
*
* @param name the name
* @param value the value
* @param domain the domain
* @param path the path
* @param maxAge the maximum age
* @param comment the comment (only present on versions &gt; 0)
* @param isSecure true if secure cookie
* @param isHttpOnly true if for http only
* @param version version of cookie logic to use (0 == default behavior)
*/
public void addSetRFC2965Cookie(
final String name,
final String value,
final String domain,
final String path,
final long maxAge,
final String comment,
final boolean isSecure,
final boolean isHttpOnly,
int version)
{
String set_cookie = newRFC2965SetCookie(name, value, domain, path, maxAge, comment, isSecure, isHttpOnly, version);
// add the set cookie
_fields.add(HttpHeader.SET_COOKIE, set_cookie);
// Expire responses with set-cookie headers so they do not get cached.
_fields.put(__EXPIRES_01JAN1970);
}
private String newRFC2965SetCookie(String name, String value, String domain, String path, long maxAge, String comment, boolean isSecure, boolean isHttpOnly, int version)
{
// Check arguments
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Bad cookie name");
// Format value and params
StringBuilder buf = __cookieBuilder.get();
buf.setLength(0);
// Name is checked for legality by servlet spec, but can also be passed directly so check again for quoting
boolean quote_name=isQuoteNeededForCookie(name);
quoteOnlyOrAppend(buf,name,quote_name);
buf.append('=');
// Append the value
boolean quote_value=isQuoteNeededForCookie(value);
quoteOnlyOrAppend(buf,value,quote_value);
// Look for domain and path fields and check if they need to be quoted
boolean has_domain = domain!=null && domain.length()>0;
boolean quote_domain = has_domain && isQuoteNeededForCookie(domain);
boolean has_path = path!=null && path.length()>0;
boolean quote_path = has_path && isQuoteNeededForCookie(path);
// Upgrade the version if we have a comment or we need to quote value/path/domain or if they were already quoted
if (version==0 && ( comment!=null || quote_name || quote_value || quote_domain || quote_path ||
QuotedStringTokenizer.isQuoted(name) || QuotedStringTokenizer.isQuoted(value) ||
QuotedStringTokenizer.isQuoted(path) || QuotedStringTokenizer.isQuoted(domain)))
version=1;
// Append version
if (version==1)
buf.append (";Version=1");
else if (version>1)
buf.append (";Version=").append(version);
// Append path
if (has_path)
{
buf.append(";Path=");
quoteOnlyOrAppend(buf,path,quote_path);
}
// Append domain
if (has_domain)
{
buf.append(";Domain=");
quoteOnlyOrAppend(buf,domain,quote_domain);
}
// Handle max-age and/or expires
if (maxAge >= 0)
{
// Always use expires
// This is required as some browser (M$ this means you!) don't handle max-age even with v1 cookies
buf.append(";Expires=");
if (maxAge == 0)
buf.append(__01Jan1970_COOKIE);
else
DateGenerator.formatCookieDate(buf, System.currentTimeMillis() + 1000L * maxAge);
// for v1 cookies, also send max-age
if (version>=1)
{
buf.append(";Max-Age=");
buf.append(maxAge);
}
}
// add the other fields
if (isSecure)
buf.append(";Secure");
if (isHttpOnly)
buf.append(";HttpOnly");
if (comment != null)
{
buf.append(";Comment=");
quoteOnlyOrAppend(buf,comment,isQuoteNeededForCookie(comment));
}
return buf.toString();
}
/* ------------------------------------------------------------ */
/** Does a cookie value need to be quoted?
* @param s value string
* @return true if quoted;
* @throws IllegalArgumentException If there a control characters in the string
*/
private static boolean isQuoteNeededForCookie(String s)
{
if (s==null || s.length()==0)
return true;
if (QuotedStringTokenizer.isQuoted(s))
return false;
for (int i=0;i<s.length();i++)
{
char c = s.charAt(i);
if (__COOKIE_DELIM.indexOf(c)>=0)
return true;
if (c<0x20 || c>=0x7f)
throw new IllegalArgumentException("Illegal character in cookie value");
}
return false;
}
private static void quoteOnlyOrAppend(StringBuilder buf, String s, boolean quote)
{
if (quote)
QuotedStringTokenizer.quoteOnly(buf,s);
else
buf.append(s);
}
@Override
public boolean containsHeader(String name)

View File

@ -1102,6 +1102,32 @@ public class ResponseTest
assertThat("HttpCookie order", actual, hasItems(expected));
}
@Test
public void testReplaceParsedHttpCookie()
{
Response response = getResponse();
response.addHeader(HttpHeader.SET_COOKIE.asString(), "Foo=123456");
response.replaceCookie(new HttpCookie("Foo","value"));
List<String> actual = Collections.list(response.getHttpFields().getValues("Set-Cookie"));
assertThat(actual, hasItems(new String[] {"Foo=value"}));
response.setHeader(HttpHeader.SET_COOKIE,"Foo=123456; domain=Bah; Path=/path");
response.replaceCookie(new HttpCookie("Foo","other"));
actual = Collections.list(response.getHttpFields().getValues("Set-Cookie"));
assertThat(actual, hasItems(new String[] {"Foo=123456; domain=Bah; Path=/path", "Foo=other"}));
response.replaceCookie(new HttpCookie("Foo","replaced", "Bah", "/path"));
actual = Collections.list(response.getHttpFields().getValues("Set-Cookie"));
assertThat(actual, hasItems(new String[] {"Foo=replaced; Path=/path; Domain=Bah", "Foo=other"}));
response.setHeader(HttpHeader.SET_COOKIE,"Foo=123456; domain=Bah; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly; Path=/path");
response.replaceCookie(new HttpCookie("Foo","replaced", "Bah", "/path"));
actual = Collections.list(response.getHttpFields().getValues("Set-Cookie"));
assertThat(actual, hasItems(new String[] {"Foo=replaced; Path=/path; Domain=Bah"}));
}
@Test
public void testFlushAfterFullContent() throws Exception
{
@ -1114,312 +1140,6 @@ public class ResponseTest
// Must not throw
output.flush();
}
@Test
public void testSetRFC2965Cookie() throws Exception
{
Response response = _channel.getResponse();
HttpFields fields = response.getHttpFields();
response.addSetRFC2965Cookie("null",null,null,null,-1,null,false,false,-1);
assertEquals("null=",fields.get("Set-Cookie"));
fields.clear();
response.addSetRFC2965Cookie("minimal","value",null,null,-1,null,false,false,-1);
assertEquals("minimal=value",fields.get("Set-Cookie"));
fields.clear();
//test cookies with same name, domain and path
response.addSetRFC2965Cookie("everything","something","domain","path",0,"noncomment",true,true,0);
response.addSetRFC2965Cookie("everything","value","domain","path",0,"comment",true,true,0);
Enumeration<String> e =fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertEquals("everything=something;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=noncomment",e.nextElement());
assertEquals("everything=value;Version=1;Path=path;Domain=domain;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",e.nextElement());
assertFalse(e.hasMoreElements());
assertEquals("Thu, 01 Jan 1970 00:00:00 GMT",fields.get("Expires"));
assertFalse(e.hasMoreElements());
//test cookies with same name, different domain
fields.clear();
response.addSetRFC2965Cookie("everything","other","domain1","path",0,"blah",true,true,0);
response.addSetRFC2965Cookie("everything","value","domain2","path",0,"comment",true,true,0);
e =fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertEquals("everything=other;Version=1;Path=path;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=blah",e.nextElement());
assertTrue(e.hasMoreElements());
assertEquals("everything=value;Version=1;Path=path;Domain=domain2;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",e.nextElement());
assertFalse(e.hasMoreElements());
//test cookies with same name, same path, one with domain, one without
fields.clear();
response.addSetRFC2965Cookie("everything","other","domain1","path",0,"blah",true,true,0);
response.addSetRFC2965Cookie("everything","value","","path",0,"comment",true,true,0);
e =fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertEquals("everything=other;Version=1;Path=path;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=blah",e.nextElement());
assertTrue(e.hasMoreElements());
assertEquals("everything=value;Version=1;Path=path;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",e.nextElement());
assertFalse(e.hasMoreElements());
//test cookies with same name, different path
fields.clear();
response.addSetRFC2965Cookie("everything","other","domain1","path1",0,"blah",true,true,0);
response.addSetRFC2965Cookie("everything","value","domain1","path2",0,"comment",true,true,0);
e =fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertEquals("everything=other;Version=1;Path=path1;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=blah",e.nextElement());
assertTrue(e.hasMoreElements());
assertEquals("everything=value;Version=1;Path=path2;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",e.nextElement());
assertFalse(e.hasMoreElements());
//test cookies with same name, same domain, one with path, one without
fields.clear();
response.addSetRFC2965Cookie("everything","other","domain1","path1",0,"blah",true,true,0);
response.addSetRFC2965Cookie("everything","value","domain1","",0,"comment",true,true,0);
e =fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertEquals("everything=other;Version=1;Path=path1;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=blah",e.nextElement());
assertTrue(e.hasMoreElements());
assertEquals("everything=value;Version=1;Domain=domain1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",e.nextElement());
assertFalse(e.hasMoreElements());
//test cookies same name only, no path, no domain
fields.clear();
response.addSetRFC2965Cookie("everything","other","","",0,"blah",true,true,0);
response.addSetRFC2965Cookie("everything","value","","",0,"comment",true,true,0);
e =fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertEquals("everything=other;Version=1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=blah",e.nextElement());
assertEquals("everything=value;Version=1;Expires=Thu, 01-Jan-1970 00:00:00 GMT;Max-Age=0;Secure;HttpOnly;Comment=comment",e.nextElement());
assertFalse(e.hasMoreElements());
fields.clear();
response.addSetRFC2965Cookie("ev erything","va lue","do main","pa th",1,"co mment",true,true,1);
String setCookie=fields.get("Set-Cookie");
assertThat(setCookie,Matchers.startsWith("\"ev erything\"=\"va lue\";Version=1;Path=\"pa th\";Domain=\"do main\";Expires="));
assertThat(setCookie,Matchers.endsWith(" GMT;Max-Age=1;Secure;HttpOnly;Comment=\"co mment\""));
fields.clear();
response.addSetRFC2965Cookie("name","value",null,null,-1,null,false,false,0);
setCookie=fields.get("Set-Cookie");
assertEquals(-1,setCookie.indexOf("Version="));
fields.clear();
response.addSetRFC2965Cookie("name","v a l u e",null,null,-1,null,false,false,0);
setCookie=fields.get("Set-Cookie");
fields.clear();
response.addSetRFC2965Cookie("json","{\"services\":[\"cwa\", \"aa\"]}",null,null,-1,null,false,false,-1);
assertEquals("json=\"{\\\"services\\\":[\\\"cwa\\\", \\\"aa\\\"]}\"",fields.get("Set-Cookie"));
fields.clear();
response.addSetRFC2965Cookie("name","value","domain",null,-1,null,false,false,-1);
response.addSetRFC2965Cookie("name","other","domain",null,-1,null,false,false,-1);
response.addSetRFC2965Cookie("name","more","domain",null,-1,null,false,false,-1);
e = fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertThat(e.nextElement(), Matchers.startsWith("name=value"));
assertThat(e.nextElement(), Matchers.startsWith("name=other"));
assertThat(e.nextElement(), Matchers.startsWith("name=more"));
response.addSetRFC2965Cookie("foo","bar","domain",null,-1,null,false,false,-1);
response.addSetRFC2965Cookie("foo","bob","domain",null,-1,null,false,false,-1);
assertThat(fields.get("Set-Cookie"), Matchers.startsWith("name=value"));
fields.clear();
response.addSetRFC2965Cookie("name","value%=",null,null,-1,null,false,false,0);
setCookie=fields.get("Set-Cookie");
assertEquals("name=value%=",setCookie);
}
@Test
public void testSetRFC6265Cookie() throws Exception
{
Response response = _channel.getResponse();
HttpFields fields = response.getHttpFields();
response.addSetRFC6265Cookie("null",null,null,null,-1,false,false);
assertEquals("null=",fields.get("Set-Cookie"));
fields.clear();
response.addSetRFC6265Cookie("minimal","value",null,null,-1,false,false);
assertEquals("minimal=value",fields.get("Set-Cookie"));
fields.clear();
//test cookies with same name, domain and path
response.addSetRFC6265Cookie("everything","something","domain","path",0,true,true);
response.addSetRFC6265Cookie("everything","value","domain","path",0,true,true);
Enumeration<String> e =fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertEquals("everything=something; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",e.nextElement());
assertEquals("everything=value; Path=path; Domain=domain; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",e.nextElement());
assertFalse(e.hasMoreElements());
assertEquals("Thu, 01 Jan 1970 00:00:00 GMT",fields.get("Expires"));
assertFalse(e.hasMoreElements());
//test cookies with same name, different domain
fields.clear();
response.addSetRFC6265Cookie("everything","other","domain1","path",0,true,true);
response.addSetRFC6265Cookie("everything","value","domain2","path",0,true,true);
e =fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertEquals("everything=other; Path=path; Domain=domain1; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",e.nextElement());
assertTrue(e.hasMoreElements());
assertEquals("everything=value; Path=path; Domain=domain2; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",e.nextElement());
assertFalse(e.hasMoreElements());
//test cookies with same name, same path, one with domain, one without
fields.clear();
response.addSetRFC6265Cookie("everything","other","domain1","path",0,true,true);
response.addSetRFC6265Cookie("everything","value","","path",0,true,true);
e =fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertEquals("everything=other; Path=path; Domain=domain1; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",e.nextElement());
assertTrue(e.hasMoreElements());
assertEquals("everything=value; Path=path; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",e.nextElement());
assertFalse(e.hasMoreElements());
//test cookies with same name, different path
fields.clear();
response.addSetRFC6265Cookie("everything","other","domain1","path1",0,true,true);
response.addSetRFC6265Cookie("everything","value","domain1","path2",0,true,true);
e =fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertEquals("everything=other; Path=path1; Domain=domain1; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",e.nextElement());
assertTrue(e.hasMoreElements());
assertEquals("everything=value; Path=path2; Domain=domain1; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",e.nextElement());
assertFalse(e.hasMoreElements());
//test cookies with same name, same domain, one with path, one without
fields.clear();
response.addSetRFC6265Cookie("everything","other","domain1","path1",0,true,true);
response.addSetRFC6265Cookie("everything","value","domain1","",0,true,true);
e =fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertEquals("everything=other; Path=path1; Domain=domain1; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",e.nextElement());
assertTrue(e.hasMoreElements());
assertEquals("everything=value; Domain=domain1; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",e.nextElement());
assertFalse(e.hasMoreElements());
//test cookies same name only, no path, no domain
fields.clear();
response.addSetRFC6265Cookie("everything","other","","",0,true,true);
response.addSetRFC6265Cookie("everything","value","","",0,true,true);
e =fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertEquals("everything=other; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",e.nextElement());
assertEquals("everything=value; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Secure; HttpOnly",e.nextElement());
assertFalse(e.hasMoreElements());
String badNameExamples[] = {
"\"name\"",
"name\t",
"na me",
"name\u0082",
"na\tme",
"na;me",
"{name}",
"[name]",
"\""
};
for (String badNameExample : badNameExamples)
{
fields.clear();
try
{
response.addSetRFC6265Cookie(badNameExample, "value", null, "/", 1, true, true);
}
catch (IllegalArgumentException ex)
{
// System.err.printf("%s: %s%n", ex.getClass().getSimpleName(), ex.getMessage());
assertThat("Testing bad name: [" + badNameExample + "]", ex.getMessage(),
allOf(containsString("RFC6265"), containsString("RFC2616")));
}
}
String badValueExamples[] = {
"va\tlue",
"\t",
"value\u0000",
"val\u0082ue",
"va lue",
"va;lue",
"\"value",
"value\"",
"val\\ue",
"val\"ue",
"\""
};
for (String badValueExample : badValueExamples)
{
fields.clear();
try
{
response.addSetRFC6265Cookie("name", badValueExample, null, "/", 1, true, true);
}
catch (IllegalArgumentException ex)
{
// System.err.printf("%s: %s%n", ex.getClass().getSimpleName(), ex.getMessage());
assertThat("Testing bad value [" + badValueExample + "]", ex.getMessage(), Matchers.containsString("RFC6265"));
}
}
String goodNameExamples[] = {
"name",
"n.a.m.e",
"na-me",
"+name",
"na*me",
"na$me",
"#name"
};
for (String goodNameExample : goodNameExamples)
{
fields.clear();
response.addSetRFC6265Cookie(goodNameExample, "value", null, "/", 1, true, true);
// should not throw an exception
}
String goodValueExamples[] = {
"value",
"",
null,
"val=ue",
"val-ue",
"val/ue",
"v.a.l.u.e"
};
for (String goodValueExample : goodValueExamples)
{
fields.clear();
response.addSetRFC6265Cookie("name", goodValueExample, null, "/", 1, true, true);
// should not throw an exception
}
fields.clear();
response.addSetRFC6265Cookie("name","value","domain",null,-1,false,false);
response.addSetRFC6265Cookie("name","other","domain",null,-1,false,false);
response.addSetRFC6265Cookie("name","more","domain",null,-1,false,false);
e = fields.getValues("Set-Cookie");
assertTrue(e.hasMoreElements());
assertThat(e.nextElement(), Matchers.startsWith("name=value"));
assertThat(e.nextElement(), Matchers.startsWith("name=other"));
assertThat(e.nextElement(), Matchers.startsWith("name=more"));
response.addSetRFC6265Cookie("foo","bar","domain",null,-1,false,false);
response.addSetRFC6265Cookie("foo","bob","domain",null,-1,false,false);
assertThat(fields.get("Set-Cookie"), Matchers.startsWith("name=value"));
}
private Response getResponse()
{