diff --git a/modules/org.restlet.test/pom.xml b/modules/org.restlet.test/pom.xml index d8c95c6db4..dd928003c1 100644 --- a/modules/org.restlet.test/pom.xml +++ b/modules/org.restlet.test/pom.xml @@ -12,7 +12,7 @@ Restlet Unit Tests All Restlet unit tests. - ${basedir}/src + ${basedir}/src/main/java diff --git a/modules/org.restlet.test/src/main/java/org/restlet/test/engine/header/CookieSettingReaderTestCase.java b/modules/org.restlet.test/src/main/java/org/restlet/test/engine/header/CookieSettingReaderTestCase.java new file mode 100644 index 0000000000..56f1d52ace --- /dev/null +++ b/modules/org.restlet.test/src/main/java/org/restlet/test/engine/header/CookieSettingReaderTestCase.java @@ -0,0 +1,31 @@ +package org.restlet.test.engine.header; + +import org.restlet.data.CookieSetting; +import org.restlet.data.CookieSetting.SameSite; +import org.restlet.engine.header.CookieSettingReader; +import org.restlet.test.RestletTestCase; + +public class CookieSettingReaderTestCase extends RestletTestCase { + + public void testReadingWithoutSameSite() { + CookieSetting readSetting = CookieSettingReader.read("cookie=value"); + assertNull(readSetting.getSameSite()); + } + + public void testReadingOfSameSite() { + for(SameSite sameSite : SameSite.values()) { + CookieSetting readSetting = CookieSettingReader.read("cookie=value; SameSite=" + sameSite); + assertEquals(sameSite, readSetting.getSameSite()); + } + } + + public void testReadingOfInvalidSameSite() { + CookieSetting readSetting = CookieSettingReader.read("cookie=value; SameSite=InvalidSameSiteValue"); + assertNull( readSetting.getSameSite()); + } + + public void testReadingOfEmptySameSite() { + CookieSetting readSetting = CookieSettingReader.read("cookie=value; SameSite="); + assertNull( readSetting.getSameSite()); + } +} diff --git a/modules/org.restlet.test/src/main/java/org/restlet/test/engine/header/CookieSettingWriterTestCase.java b/modules/org.restlet.test/src/main/java/org/restlet/test/engine/header/CookieSettingWriterTestCase.java new file mode 100644 index 0000000000..7a214275be --- /dev/null +++ b/modules/org.restlet.test/src/main/java/org/restlet/test/engine/header/CookieSettingWriterTestCase.java @@ -0,0 +1,24 @@ +package org.restlet.test.engine.header; + +import org.restlet.data.CookieSetting; +import org.restlet.data.CookieSetting.SameSite; +import org.restlet.engine.header.CookieSettingWriter; +import org.restlet.test.RestletTestCase; + +public class CookieSettingWriterTestCase extends RestletTestCase { + + public void testWritingOfUnsetSameSite() { + CookieSetting testSetting = new CookieSetting("cookie", "value"); + assertEquals("cookie=value", CookieSettingWriter.write(testSetting)); + + assertEquals("cookie=value", CookieSettingWriter.write(testSetting)); + } + + public void testWritingOfSameSite() { + for(SameSite sameSite : SameSite.values()) { + CookieSetting testSetting = new CookieSetting("cookie", "value"); + testSetting.setSameSite(sameSite); + assertEquals("cookie=value; SameSite=" + sameSite, CookieSettingWriter.write(testSetting)); + } + } +} diff --git a/modules/org.restlet/src/main/java/org/restlet/data/CookieSetting.java b/modules/org.restlet/src/main/java/org/restlet/data/CookieSetting.java index c529e2c9de..4823ae1598 100644 --- a/modules/org.restlet/src/main/java/org/restlet/data/CookieSetting.java +++ b/modules/org.restlet/src/main/java/org/restlet/data/CookieSetting.java @@ -58,6 +58,24 @@ public final class CookieSetting extends Cookie { /** Indicates if cookie should only be transmitted by secure means. */ private volatile boolean secure; + /** Explicitly specifies a same site policy for browsers. */ + private volatile SameSite sameSite; + + public enum SameSite { + LAX("Lax"), + STRICT("Strict"), + NONE("None"); + + final String value; + SameSite(String value) { + this.value = value; + } + + public String toString() { + return value; + } + } + /** * Default constructor. */ @@ -164,6 +182,47 @@ public CookieSetting(int version, String name, String value, String path, this.secure = secure; this.accessRestricted = accessRestricted; } + + /** + * Constructor. + * + * @param version + * The cookie's version. + * @param name + * The cookie's name. + * @param value + * The cookie's value. + * @param path + * The cookie's path. + * @param domain + * The cookie's domain name. + * @param comment + * The cookie's comment. + * @param maxAge + * Sets the maximum age in seconds.
+ * Use 0 to immediately discard an existing cookie.
+ * Use -1 to discard the cookie at the end of the session + * (default). + * @param secure + * Indicates if cookie should only be transmitted by secure + * means. + * @param accessRestricted + * Indicates whether to restrict cookie access to untrusted + * parties. Currently this toggles the non-standard but widely + * supported HttpOnly cookie parameter. + * @param sameSite + * The cookie's same site policy. + */ + public CookieSetting(int version, String name, String value, String path, + String domain, String comment, int maxAge, boolean secure, + boolean accessRestricted, SameSite sameSite) { + super(version, name, value, path, domain); + this.comment = comment; + this.maxAge = maxAge; + this.secure = secure; + this.accessRestricted = accessRestricted; + this.sameSite = sameSite; + } /** * Preferred constructor. @@ -192,7 +251,8 @@ public boolean equals(Object obj) { return super.equals(obj) && this.maxAge == that.maxAge && this.secure == that.secure - && Objects.equals(this.comment, that.comment); + && Objects.equals(this.comment, that.comment) + && Objects.equals(this.sameSite, that.sameSite); } /** @@ -228,7 +288,7 @@ public int getMaxAge() { @Override public int hashCode() { return SystemUtils.hashCode(super.hashCode(), getComment(), - getMaxAge(), isSecure()); + getMaxAge(), isSecure(), getSameSite()); } /** @@ -251,6 +311,17 @@ public boolean isSecure() { return this.secure; } + + /** + * Returns the currently set same site policy. + * + * @return sameSite + * The currently set same site attribute setting. + */ + public SameSite getSameSite() { + return this.sameSite; + } + /** * Indicates whether to restrict cookie access to untrusted parties. * Currently this toggles the non-standard but widely supported HttpOnly @@ -293,6 +364,16 @@ public void setMaxAge(int maxAge) { public void setSecure(boolean secure) { this.secure = secure; } + + /** + * Sets the same site policy for the browser to apply to this cookie. + * + * @param sameSite + * The new same site policy to set. + */ + public void setSameSite(SameSite sameSite) { + this.sameSite = sameSite; + } @Override public String toString() { @@ -300,7 +381,8 @@ public String toString() { + ", comment=" + comment + ", maxAge=" + maxAge + ", secure=" + secure + ", domain=" + getDomain() + ", name=" + getName() + ", path=" + getPath() + ", value=" + getValue() - + ", version=" + getVersion() + "]"; + + ", version=" + getVersion() + +", sameSite=" + "]"; } } diff --git a/modules/org.restlet/src/main/java/org/restlet/engine/header/CookieSettingReader.java b/modules/org.restlet/src/main/java/org/restlet/engine/header/CookieSettingReader.java index ce7adb49c9..7c739bdf1d 100644 --- a/modules/org.restlet/src/main/java/org/restlet/engine/header/CookieSettingReader.java +++ b/modules/org.restlet/src/main/java/org/restlet/engine/header/CookieSettingReader.java @@ -34,6 +34,7 @@ import org.restlet.Context; import org.restlet.data.CookieSetting; import org.restlet.data.Parameter; +import org.restlet.data.CookieSetting.SameSite; import org.restlet.engine.util.DateUtils; import org.restlet.engine.util.StringUtils; @@ -65,6 +66,8 @@ public class CookieSettingReader extends HeaderReader { private static final String NAME_SET_SECURE = "secure"; private static final String NAME_SET_VERSION = "version"; + + private static final String NAME_SET_SAME_SITE ="samesite"; /** * Parses the given String to a CookieSetting @@ -243,6 +246,16 @@ public CookieSetting readValue() throws IOException { } } else if (pair.getName().equalsIgnoreCase(NAME_SET_VERSION)) { result.setVersion(Integer.valueOf(pair.getValue())); + } else if(pair.getName().equalsIgnoreCase(NAME_SET_SAME_SITE) && !"".equals(pair.getValue())) { + SameSite sameSite = null; + try { + sameSite = SameSite.valueOf(pair.getValue().toUpperCase()); + } catch(IllegalArgumentException illigalArgumentException) { + Context.getCurrentLogger() + .warning("Unable to parse cookie setting same-site value \"" + pair.getValue() + + "\". Not setting same-site attribute."); + } + result.setSameSite(sameSite); } else { // Unexpected special attribute // Silently ignore it as it may have been introduced by new specifications diff --git a/modules/org.restlet/src/main/java/org/restlet/engine/header/CookieSettingWriter.java b/modules/org.restlet/src/main/java/org/restlet/engine/header/CookieSettingWriter.java index 72ba546056..6c09abb447 100644 --- a/modules/org.restlet/src/main/java/org/restlet/engine/header/CookieSettingWriter.java +++ b/modules/org.restlet/src/main/java/org/restlet/engine/header/CookieSettingWriter.java @@ -28,6 +28,7 @@ import java.util.List; import org.restlet.data.CookieSetting; +import org.restlet.data.CookieSetting.SameSite; import org.restlet.engine.util.DateUtils; /** @@ -142,6 +143,13 @@ public CookieSettingWriter append(CookieSetting cookieSetting) if (cookieSetting.isAccessRestricted()) { append("; HttpOnly"); } + + // Append the same site attribute if it is set. + SameSite sameSite = cookieSetting.getSameSite(); + if(sameSite != null) { + append("; SameSite="); + appendValue(sameSite.toString(), version); + } // Append the comment if (version > 0) {