From 5fa9a75724fcbf2df0192f88a57479503b741eb3 Mon Sep 17 00:00:00 2001 From: Gerry Gao Date: Tue, 10 Dec 2019 13:06:40 -0800 Subject: [PATCH] Add `SameSite` attribute to `Cookie` Cookie object in Selenium Java doesn't support setting `SameSite` attribute, described in the spce: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-5.3.7 ChromeDriver already supports it. So improving Selenium Java API to support it as well. Partially fix #7798 Signed-off-by: Alexei Barantsev --- .../src/org/openqa/selenium/Cookie.java | 45 ++++++++++++++++++- .../selenium/remote/RemoteWebDriver.java | 5 ++- .../test/org/openqa/selenium/CookieTest.java | 11 ++++- .../remote/RemoteWebDriverUnitTest.java | 6 ++- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/java/client/src/org/openqa/selenium/Cookie.java b/java/client/src/org/openqa/selenium/Cookie.java index d8b90bdec4a92..eb84506ca914a 100644 --- a/java/client/src/org/openqa/selenium/Cookie.java +++ b/java/client/src/org/openqa/selenium/Cookie.java @@ -33,6 +33,7 @@ public class Cookie implements Serializable { private final Date expiry; private final boolean isSecure; private final boolean isHttpOnly; + private final String sameSite; /** * Creates an insecure non-httpOnly cookie with no domain specified. @@ -93,6 +94,24 @@ public Cookie(String name, String value, String domain, String path, Date expiry */ public Cookie(String name, String value, String domain, String path, Date expiry, boolean isSecure, boolean isHttpOnly) { + this(name, value, domain, path, expiry, isSecure, isHttpOnly, null); + } + + /** + * Creates a cookie. + * + * @param name The name of the cookie; may not be null or an empty string. + * @param value The cookie value; may not be null. + * @param domain The domain the cookie is visible to. + * @param path The path the cookie is visible to. If left blank or set to null, will be set to + * "/". + * @param expiry The cookie's expiration date; may be null. + * @param isSecure Whether this cookie requires a secure connection. + * @param isHttpOnly Whether this cookie is a httpOnly cookie. + * @param sameSite The samesite attribute of this cookie; e.g. None, Lax, Strict. + */ + public Cookie(String name, String value, String domain, String path, Date expiry, + boolean isSecure, boolean isHttpOnly, String sameSite) { this.name = name; this.value = value; this.path = path == null || "".equals(path) ? "/" : path; @@ -107,6 +126,8 @@ public Cookie(String name, String value, String domain, String path, Date expiry } else { this.expiry = null; } + + this.sameSite = sameSite; } /** @@ -158,6 +179,10 @@ public Date getExpiry() { return expiry; } + public String getSameSite() { + return sameSite; + } + private static String stripPort(String domain) { return (domain == null) ? null : domain.split(":")[0]; } @@ -178,6 +203,10 @@ public void validate() { } } + /** + * JSON object keys are defined in + * https://w3c.github.io/webdriver/#dfn-table-for-cookie-conversion. + */ public Map toJson() { Map toReturn = new TreeMap<>(); @@ -204,6 +233,10 @@ public Map toJson() { toReturn.put("secure", isSecure()); toReturn.put("httpOnly", isHttpOnly()); + if (getSameSite() != null) { + toReturn.put("samesite", getSameSite()); + } + return toReturn; } @@ -215,7 +248,8 @@ public String toString() { .format(expiry)) + ("".equals(path) ? "" : "; path=" + path) + (domain == null ? "" : "; domain=" + domain) - + (isSecure ? ";secure;" : ""); + + (isSecure ? ";secure;" : "") + + (sameSite == null ? "" : "; sameSite=" + sameSite); } /** @@ -252,6 +286,7 @@ public static class Builder { private Date expiry; private boolean secure; private boolean httpOnly; + private String sameSite; public Builder(String name, String value) { this.name = name; @@ -283,8 +318,14 @@ public Builder isHttpOnly(boolean httpOnly) { return this; } + public Builder sameSite(String sameSite) { + this.sameSite = sameSite; + return this; + } + public Cookie build() { - return new Cookie(name, value, domain, path, expiry, secure, httpOnly); + return new Cookie( + name, value, domain, path, expiry, secure, httpOnly, sameSite); } } } diff --git a/java/client/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/client/src/org/openqa/selenium/remote/RemoteWebDriver.java index 45593aa630c64..dabce3502c0ed 100644 --- a/java/client/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/client/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -762,13 +762,16 @@ public Set getCookies() { ((Collection) returned).stream() .map(o -> (Map) o) .map(rawCookie -> { + // JSON object keys are defined in + // https://w3c.github.io/webdriver/#dfn-table-for-cookie-conversion. Cookie.Builder builder = new Cookie.Builder((String) rawCookie.get("name"), (String) rawCookie.get("value")) .path((String) rawCookie.get("path")) .domain((String) rawCookie.get("domain")) .isSecure(rawCookie.containsKey("secure") && (Boolean) rawCookie.get("secure")) .isHttpOnly( - rawCookie.containsKey("httpOnly") && (Boolean) rawCookie.get("httpOnly")); + rawCookie.containsKey("httpOnly") && (Boolean) rawCookie.get("httpOnly")) + .sameSite((String) rawCookie.get("samesite")); Number expiryNum = (Number) rawCookie.get("expiry"); builder.expiresOn(expiryNum == null ? null : new Date(SECONDS.toMillis(expiryNum.longValue()))); diff --git a/java/client/test/org/openqa/selenium/CookieTest.java b/java/client/test/org/openqa/selenium/CookieTest.java index 4e8ca5ecc5562..eb1fd5b0bb63b 100644 --- a/java/client/test/org/openqa/selenium/CookieTest.java +++ b/java/client/test/org/openqa/selenium/CookieTest.java @@ -74,11 +74,20 @@ public void testHttpOnlyDefaultsToFalse() { assertThat(cookie.isHttpOnly()).isFalse(); } + @Test + public void testCookiesShouldAllowSameSiteToBeSet() { + Cookie cookie = new Cookie("name", "value", "", "/", new Date(), false, true, "Lax"); + assertThat(cookie.getSameSite()).isEqualTo("Lax"); + + Cookie builderCookie = new Cookie.Builder("name", "value").sameSite("Lax").build(); + assertThat(builderCookie.getSameSite()).isEqualTo("Lax"); + } + @Test public void testCookieSerializes() throws IOException, ClassNotFoundException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); - Cookie cookieToSerialize = new Cookie("Fish", "cod", "", "", null, false); + Cookie cookieToSerialize = new Cookie("Fish", "cod", "", "", null, false, true, "Lax"); objectOutputStream.writeObject(cookieToSerialize); byte[] serializedCookie = byteArrayOutputStream.toByteArray(); diff --git a/java/client/test/org/openqa/selenium/remote/RemoteWebDriverUnitTest.java b/java/client/test/org/openqa/selenium/remote/RemoteWebDriverUnitTest.java index 22bd6ce5cf6e9..688460dea12a6 100644 --- a/java/client/test/org/openqa/selenium/remote/RemoteWebDriverUnitTest.java +++ b/java/client/test/org/openqa/selenium/remote/RemoteWebDriverUnitTest.java @@ -623,7 +623,7 @@ public void canHandleGetCookiesCommand() throws IOException { CommandExecutor executor = prepareExecutorMock( echoCapabilities, valueResponder(ImmutableList.of( - ImmutableMap.of("name", "cookie1", "value", "value1"), + ImmutableMap.of("name", "cookie1", "value", "value1", "samesite", "Lax"), ImmutableMap.of("name", "cookie2", "value", "value2")))); RemoteWebDriver driver = new RemoteWebDriver(executor, new ImmutableCapabilities()); @@ -631,7 +631,9 @@ public void canHandleGetCookiesCommand() throws IOException { assertThat(cookies) .hasSize(2) - .contains(new Cookie("cookie1", "value1"), new Cookie("cookie2", "value2")); + .contains( + new Cookie.Builder("cookie1", "value1").sameSite("Lax").build(), + new Cookie("cookie2", "value2")); verifyCommands( executor, driver.getSessionId(), new CommandPayload(DriverCommand.GET_ALL_COOKIES, ImmutableMap.of()));