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:
parent
60cbe8b7b2
commit
e4af5c3631
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user