diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java index 74cace7385..01a35f3b95 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java @@ -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)); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java index 6d2e7f577c..fef0567218 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java @@ -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 changes to RFC6265bis + */ + 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. } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java index cbd54cd092..137a65d49a 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java @@ -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 + * here + * @return same-site-flag value + */ + public SameSite sameSite() { + return sameSite; + } + + /** + * Determines if this this {@link Cookie} can be sent along cross-site requests. + * For more information, please look + * here + * @param sameSite same-site-flag 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(); } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java index c5a1d7d0e7..b0ee21a92c 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java @@ -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 RFC6265 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); } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java index 2cd69f98dc..24e3361d17 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java @@ -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()); } diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java index 82f813ff47..42f9c21f05 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java @@ -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);