Issue #1396 RFC6265 cookie compliance

This commit is contained in:
Greg Wilkins 2017-03-15 23:20:45 +11:00
parent f81c424509
commit 3b1d33e944
7 changed files with 311 additions and 30 deletions

View File

@ -90,6 +90,7 @@
<Set name="maxErrorDispatches"><Property name="jetty.httpConfig.maxErrorDispatches" default="10"/></Set>
<Set name="blockingTimeout"><Property name="jetty.httpConfig.blockingTimeout" default="-1"/></Set>
<Set name="persistentConnectionsEnabled"><Property name="jetty.httpConfig.persistentConnectionsEnabled" default="true"/></Set>
<Set name="cookieCompliance"><Call class="org.eclipse.jetty.server.CookieCompliance" name="valueOf"><Arg><Property name="jetty.httpConfig.cookieCompliance" default="RFC6265"/></Arg></Call></Set>
</New>
<!-- =========================================================== -->

View File

@ -67,6 +67,9 @@ etc/jetty.xml
## Maximum time to block in total for a blocking IO operation (default -1 is to use idleTimeout on progress)
# jetty.httpConfig.blockingTimeout=-1
## Cookie compliance mode of: RFC2965, RFC6265
# jetty.httpConfig.cookieCompliance=RFC6265
### Server configuration
## Whether ctrl+c on the console gracefully stops the Jetty server
# jetty.server.stopAtShutdown=true
@ -85,3 +88,4 @@ etc/jetty.xml
## How frequently sessions are inspected
#jetty.sessionInspectionInterval.seconds=60

View File

@ -0,0 +1,25 @@
//
// ========================================================================
// Copyright (c) 1995-2017 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.server;
/**
* The compliance for Cookie handling.
*
*/
public enum CookieCompliance { RFC6265, RFC2965 }

View File

@ -275,8 +275,11 @@ public class HttpChannelOverHttp extends HttpChannel implements HttpParser.Reque
@Override
public boolean headerComplete()
{
if (_complianceViolations != null)
if (_complianceViolations != null && !_complianceViolations.isEmpty())
{
this.getRequest().setAttribute(ATTR_COMPLIANCE_VIOLATIONS, _complianceViolations);
_complianceViolations=null;
}
boolean persistent;

View File

@ -66,6 +66,7 @@ public class HttpConfiguration
private boolean _persistentConnectionsEnabled = true;
private int _maxErrorDispatches = 10;
private long _minRequestDataRate;
private CookieCompliance _cookieCompliance = CookieCompliance.RFC6265;
/* ------------------------------------------------------------ */
/**
@ -124,6 +125,7 @@ public class HttpConfiguration
_persistentConnectionsEnabled=config._persistentConnectionsEnabled;
_maxErrorDispatches=config._maxErrorDispatches;
_minRequestDataRate=config._minRequestDataRate;
_cookieCompliance=config._cookieCompliance;
}
/* ------------------------------------------------------------ */
@ -527,4 +529,22 @@ public class HttpConfiguration
{
_minRequestDataRate=bytesPerSecond;
}
/* ------------------------------------------------------------ */
public CookieCompliance getCookieCompliance()
{
return _cookieCompliance;
}
/* ------------------------------------------------------------ */
public void setCookieCompliance(CookieCompliance cookieCompliance)
{
_cookieCompliance = cookieCompliance==null?CookieCompliance.RFC6265:cookieCompliance;
}
/* ------------------------------------------------------------ */
public boolean isCookieCompliance(CookieCompliance compliance)
{
return _cookieCompliance.equals(compliance);
}
}

View File

@ -172,7 +172,8 @@ public class Response implements HttpServletResponse
public void addCookie(HttpCookie cookie)
{
addSetCookie(
if (getHttpChannel().getHttpConfiguration().isCookieCompliance(CookieCompliance.RFC2965))
addSetRFC2965Cookie(
cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
@ -182,6 +183,15 @@ public class Response implements HttpServletResponse
cookie.isSecure(),
cookie.isHttpOnly(),
cookie.getVersion());
else
addSetRFC6265Cookie(
cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
cookie.getMaxAge(),
cookie.isSecure(),
cookie.isHttpOnly());
}
@Override
@ -201,7 +211,9 @@ public class Response implements HttpServletResponse
comment = null;
}
}
addSetCookie(cookie.getName(),
if (getHttpChannel().getHttpConfiguration().isCookieCompliance(CookieCompliance.RFC2965))
addSetRFC2965Cookie(cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
@ -210,9 +222,90 @@ public class Response implements HttpServletResponse
cookie.getSecure(),
httpOnly || cookie.isHttpOnly(),
cookie.getVersion());
else
addSetRFC6265Cookie(cookie.getName(),
cookie.getValue(),
cookie.getDomain(),
cookie.getPath(),
cookie.getMaxAge(),
cookie.getSecure(),
httpOnly || cookie.isHttpOnly());
}
/**
* 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)
{
// 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
boolean quote_name=isQuoteNeededForCookie(name);
boolean quote_value=value==null?false:isQuoteNeededForCookie(value);
if (quote_name || quote_value)
throw new IllegalArgumentException("Cookie name or value not RFC6265 compliant");
// 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");
// add the set cookie
_fields.add(HttpHeader.SET_COOKIE, buf.toString());
// Expire responses with set-cookie headers so they do not get cached.
_fields.put(__EXPIRES_01JAN1970);
}
/**
* Format a set cookie value
*
@ -226,7 +319,7 @@ public class Response implements HttpServletResponse
* @param isHttpOnly true if for http only
* @param version version of cookie logic to use (0 == default behavior)
*/
public void addSetCookie(
public void addSetRFC2965Cookie(
final String name,
final String value,
final String domain,

View File

@ -56,6 +56,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
@ -881,6 +882,25 @@ public class ResponseTest
String set = response.getHttpFields().get("Set-Cookie");
assertEquals("name=value;Path=/path;Domain=domain;Secure;HttpOnly", set);
}
@Test
public void testAddCookieComplianceRFC2965() throws Exception
{
Response response = getResponse();
response.getHttpChannel().getHttpConfiguration().setCookieCompliance(CookieCompliance.RFC2965);
Cookie cookie = new Cookie("name", "value");
cookie.setDomain("domain");
cookie.setPath("/path");
cookie.setSecure(true);
cookie.setComment("comment__HTTP_ONLY__");
response.addCookie(cookie);
String set = response.getHttpFields().get("Set-Cookie");
assertEquals("name=value;Version=1;Path=/path;Domain=domain;Secure;HttpOnly;Comment=comment", set);
}
@ -941,7 +961,7 @@ public class ResponseTest
assertNotNull(set);
ArrayList<String> list = Collections.list(set);
assertEquals(2, list.size());
assertTrue(list.contains("name=value;Version=1;Path=/path;Domain=domain;Secure;HttpOnly;Comment=comment"));
assertTrue(list.contains("name=value;Path=/path;Domain=domain;Secure;HttpOnly"));
assertTrue(list.contains("name2=value2;Path=/path;Domain=domain"));
//get rid of the cookies
@ -965,23 +985,23 @@ public class ResponseTest
}
@Test
public void testSetCookie() throws Exception
public void testSetRFC2965Cookie() throws Exception
{
Response response = _channel.getResponse();
HttpFields fields = response.getHttpFields();
response.addSetCookie("null",null,null,null,-1,null,false,false,-1);
response.addSetRFC2965Cookie("null",null,null,null,-1,null,false,false,-1);
assertEquals("null=",fields.get("Set-Cookie"));
fields.clear();
response.addSetCookie("minimal","value",null,null,-1,null,false,false,-1);
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.addSetCookie("everything","something","domain","path",0,"noncomment",true,true,0);
response.addSetCookie("everything","value","domain","path",0,"comment",true,true,0);
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());
@ -992,8 +1012,8 @@ public class ResponseTest
//test cookies with same name, different domain
fields.clear();
response.addSetCookie("everything","other","domain1","path",0,"blah",true,true,0);
response.addSetCookie("everything","value","domain2","path",0,"comment",true,true,0);
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());
@ -1003,8 +1023,8 @@ public class ResponseTest
//test cookies with same name, same path, one with domain, one without
fields.clear();
response.addSetCookie("everything","other","domain1","path",0,"blah",true,true,0);
response.addSetCookie("everything","value","","path",0,"comment",true,true,0);
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());
@ -1015,8 +1035,8 @@ public class ResponseTest
//test cookies with same name, different path
fields.clear();
response.addSetCookie("everything","other","domain1","path1",0,"blah",true,true,0);
response.addSetCookie("everything","value","domain1","path2",0,"comment",true,true,0);
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());
@ -1026,8 +1046,8 @@ public class ResponseTest
//test cookies with same name, same domain, one with path, one without
fields.clear();
response.addSetCookie("everything","other","domain1","path1",0,"blah",true,true,0);
response.addSetCookie("everything","value","domain1","",0,"comment",true,true,0);
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());
@ -1037,8 +1057,8 @@ public class ResponseTest
//test cookies same name only, no path, no domain
fields.clear();
response.addSetCookie("everything","other","","",0,"blah",true,true,0);
response.addSetCookie("everything","value","","",0,"comment",true,true,0);
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());
@ -1046,44 +1066,159 @@ public class ResponseTest
assertFalse(e.hasMoreElements());
fields.clear();
response.addSetCookie("ev erything","va lue","do main","pa th",1,"co mment",true,true,1);
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.addSetCookie("name","value",null,null,-1,null,false,false,0);
response.addSetRFC2965Cookie("name","value",null,null,-1,null,false,false,0);
setCookie=fields.get("Set-Cookie");
assertEquals(-1,setCookie.indexOf("Version="));
fields.clear();
response.addSetCookie("name","v a l u e",null,null,-1,null,false,false,0);
response.addSetRFC2965Cookie("name","v a l u e",null,null,-1,null,false,false,0);
setCookie=fields.get("Set-Cookie");
fields.clear();
response.addSetCookie("json","{\"services\":[\"cwa\", \"aa\"]}",null,null,-1,null,false,false,-1);
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.addSetCookie("name","value","domain",null,-1,null,false,false,-1);
response.addSetCookie("name","other","domain",null,-1,null,false,false,-1);
response.addSetCookie("name","more","domain",null,-1,null,false,false,-1);
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.addSetCookie("foo","bar","domain",null,-1,null,false,false,-1);
response.addSetCookie("foo","bob","domain",null,-1,null,false,false,-1);
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.addSetCookie("name","value%=",null,null,-1,null,false,false,0);
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());
fields.clear();
try
{
response.addSetRFC6265Cookie("ev erything","va lue","do main","pa th",1,true,true);
}
catch(IllegalArgumentException ex)
{
assertThat(ex.getMessage(),Matchers.containsString("RFC6265"));
}
fields.clear();
try
{
response.addSetRFC6265Cookie("everything","va lue","do main","pa th",1,true,true);
}
catch(IllegalArgumentException ex)
{
assertThat(ex.getMessage(),Matchers.containsString("RFC6265"));
}
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()
{
_channel.recycle();