Added support for the SameSite attribute in Cookies (#10050)

Motivation:

Netty currently does not support the SameSite attribute for response cookies (see issue #8161 for discussion).

Modifications:

The attribute has been added to the DefaultCookie class as a quick fix since adding new methods to the Cookie interface would be backwards-incompatible.
ServerCookieEncoder and ClientCookieDecoder have been updated accordingly to process this value. No validation for allowed values (Lax, None, Strict) has been implemented.

Result:

Response cookies with the SameSite attribute set can be read or written by Netty.

Co-authored-by: David Latorre <a-dlatorre@hotels.com>
This commit is contained in:
David Latorre 2020-03-12 08:48:30 +00:00 committed by GitHub
parent 60cbe8b7b2
commit e4af5c3631
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 89 additions and 14 deletions

View File

@ -16,6 +16,7 @@
package io.netty.handler.codec.http.cookie;
import io.netty.handler.codec.DateFormatter;
import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite;
import java.util.Date;
@ -154,6 +155,7 @@ public final class ClientCookieDecoder extends CookieDecoder {
private int expiresEnd;
private boolean secure;
private boolean httpOnly;
private SameSite sameSite;
CookieBuilder(DefaultCookie cookie, String header) {
this.cookie = cookie;
@ -180,6 +182,7 @@ public final class ClientCookieDecoder extends CookieDecoder {
cookie.setMaxAge(mergeMaxAgeAndExpires());
cookie.setSecure(secure);
cookie.setHttpOnly(httpOnly);
cookie.setSameSite(sameSite);
return cookie;
}
@ -206,7 +209,7 @@ public final class ClientCookieDecoder extends CookieDecoder {
} else if (length == 7) {
parse7(keyStart, valueStart, valueEnd);
} else if (length == 8) {
parse8(keyStart);
parse8(keyStart, valueStart, valueEnd);
}
}
@ -241,9 +244,11 @@ public final class ClientCookieDecoder extends CookieDecoder {
}
}
private void parse8(int nameStart) {
private void parse8(int nameStart, int valueStart, int valueEnd) {
if (header.regionMatches(true, nameStart, CookieHeaderNames.HTTPONLY, 0, 8)) {
httpOnly = true;
} else if (header.regionMatches(true, nameStart, CookieHeaderNames.SAMESITE, 0, 8)) {
sameSite = SameSite.of(computeValue(valueStart, valueEnd));
}
}

View File

@ -28,6 +28,35 @@ public final class CookieHeaderNames {
public static final String HTTPONLY = "HTTPOnly";
public static final String SAMESITE = "SameSite";
/**
* Possible values for the SameSite attribute.
* See <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05">changes to RFC6265bis</a>
*/
public enum SameSite {
Lax,
Strict,
None;
/**
* Return the enum value corresponding to the passed in same-site-flag, using a case insensitive comparison.
*
* @param name value for the SameSite Attribute
* @return enum value for the provided name or null
*/
static SameSite of(String name) {
if (name != null) {
for (SameSite each : SameSite.class.getEnumConstants()) {
if (each.name().equalsIgnoreCase(name)) {
return each;
}
}
}
return null;
}
}
private CookieHeaderNames() {
// Unused.
}

View File

@ -15,7 +15,10 @@
*/
package io.netty.handler.codec.http.cookie;
import static io.netty.handler.codec.http.cookie.CookieUtil.*;
import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite;
import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder;
import static io.netty.handler.codec.http.cookie.CookieUtil.validateAttributeValue;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
/**
@ -31,6 +34,7 @@ public class DefaultCookie implements Cookie {
private long maxAge = UNDEFINED_MAX_AGE;
private boolean secure;
private boolean httpOnly;
private SameSite sameSite;
/**
* Creates a new cookie with the specified name and value.
@ -119,6 +123,26 @@ public class DefaultCookie implements Cookie {
this.httpOnly = httpOnly;
}
/**
* Checks to see if this {@link Cookie} can be sent along cross-site requests.
* For more information, please look
* <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05">here</a>
* @return <b>same-site-flag</b> value
*/
public SameSite sameSite() {
return sameSite;
}
/**
* Determines if this this {@link Cookie} can be sent along cross-site requests.
* For more information, please look
* <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05">here</a>
* @param sameSite <b>same-site-flag</b> value
*/
public void setSameSite(SameSite sameSite) {
this.sameSite = sameSite;
}
@Override
public int hashCode() {
return name().hashCode();
@ -232,6 +256,9 @@ public class DefaultCookie implements Cookie {
if (isHttpOnly()) {
buf.append(", HTTPOnly");
}
if (sameSite() != null) {
buf.append(", SameSite=").append(sameSite());
}
return buf.toString();
}
}

View File

@ -15,12 +15,6 @@
*/
package io.netty.handler.codec.http.cookie;
import static io.netty.handler.codec.http.cookie.CookieUtil.add;
import static io.netty.handler.codec.http.cookie.CookieUtil.addQuoted;
import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder;
import static io.netty.handler.codec.http.cookie.CookieUtil.stripTrailingSeparator;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.handler.codec.DateFormatter;
import io.netty.handler.codec.http.HttpConstants;
import io.netty.handler.codec.http.HttpResponse;
@ -34,6 +28,12 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import static io.netty.handler.codec.http.cookie.CookieUtil.add;
import static io.netty.handler.codec.http.cookie.CookieUtil.addQuoted;
import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder;
import static io.netty.handler.codec.http.cookie.CookieUtil.stripTrailingSeparator;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
/**
* A <a href="http://tools.ietf.org/html/rfc6265">RFC6265</a> compliant cookie encoder to be used server side,
* so some fields are sent (Version is typically ignored).
@ -124,6 +124,12 @@ public final class ServerCookieEncoder extends CookieEncoder {
if (cookie.isHttpOnly()) {
add(buf, CookieHeaderNames.HTTPONLY);
}
if (cookie instanceof DefaultCookie) {
DefaultCookie c = (DefaultCookie) cookie;
if (c.sameSite() != null) {
add(buf, CookieHeaderNames.SAMESITE, c.sameSite().name());
}
}
return stripTrailingSeparator(buf);
}

View File

@ -16,6 +16,7 @@
package io.netty.handler.codec.http.cookie;
import io.netty.handler.codec.DateFormatter;
import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite;
import org.junit.Test;
import java.util.ArrayList;
@ -25,6 +26,8 @@ import java.util.Date;
import java.util.Iterator;
import java.util.TimeZone;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.*;
public class ClientCookieDecoderTest {
@ -32,7 +35,7 @@ public class ClientCookieDecoderTest {
public void testDecodingSingleCookieV0() {
String cookieString = "myCookie=myValue;expires="
+ DateFormatter.format(new Date(System.currentTimeMillis() + 50000))
+ ";path=/apathsomewhere;domain=.adomainsomewhere;secure;";
+ ";path=/apathsomewhere;domain=.adomainsomewhere;secure;SameSite=None";
Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
assertNotNull(cookie);
@ -44,6 +47,9 @@ public class ClientCookieDecoderTest {
cookie.maxAge() >= 40 && cookie.maxAge() <= 60);
assertEquals("/apathsomewhere", cookie.path());
assertTrue(cookie.isSecure());
assertThat(cookie, is(instanceOf(DefaultCookie.class)));
assertEquals(SameSite.None, ((DefaultCookie) cookie).sameSite());
}
@Test
@ -259,7 +265,7 @@ public class ClientCookieDecoderTest {
"'=KqtH";
Cookie cookie = ClientCookieDecoder.STRICT.decode("bh=\"" + longValue
+ "\";");
+ "\";");
assertEquals("bh", cookie.name());
assertEquals(longValue, cookie.value());
}

View File

@ -32,6 +32,7 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.netty.handler.codec.http.cookie.CookieHeaderNames.SameSite;
import org.junit.Test;
public class ServerCookieEncoderTest {
@ -41,13 +42,14 @@ public class ServerCookieEncoderTest {
int maxAge = 50;
String result =
"myCookie=myValue; Max-Age=50; Expires=(.+?); Path=/apathsomewhere; Domain=.adomainsomewhere; Secure";
Cookie cookie = new DefaultCookie("myCookie", "myValue");
String result = "myCookie=myValue; Max-Age=50; Expires=(.+?); Path=/apathsomewhere;" +
" Domain=.adomainsomewhere; Secure; SameSite=Lax";
DefaultCookie cookie = new DefaultCookie("myCookie", "myValue");
cookie.setDomain(".adomainsomewhere");
cookie.setMaxAge(maxAge);
cookie.setPath("/apathsomewhere");
cookie.setSecure(true);
cookie.setSameSite(SameSite.Lax);
String encodedCookie = ServerCookieEncoder.STRICT.encode(cookie);