diff --git a/docs/configuration.rst b/docs/configuration.rst index 4f96558d..7cbba3bc 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -842,6 +842,21 @@ Browser source property: ``cookie_domain`` cookie_domain = ".example.com" } +Browser source property: ``cookie_same_site`` +"""""""""""""""""""""""""""""""""""""""""" +:Description: + The cookie SameSite that is assigned to the cookies. When left empty, the cookies will be set with browser default. Available options to set - Strict, Lax & None; Secure +:Default: + *Empty* +:Example: + + .. code-block:: none + + divolte.sources.a_source { + type = browser + cookie_same_site = "Lax" + } + Browser source property: ``javascript.name`` """""""""""""""""""""""""""""""""""""""""""" :Description: diff --git a/src/main/java/io/divolte/server/config/BrowserSourceConfiguration.java b/src/main/java/io/divolte/server/config/BrowserSourceConfiguration.java index 5a13c0ac..6d3b7037 100644 --- a/src/main/java/io/divolte/server/config/BrowserSourceConfiguration.java +++ b/src/main/java/io/divolte/server/config/BrowserSourceConfiguration.java @@ -16,15 +16,6 @@ package io.divolte.server.config; -import java.time.Duration; -import java.util.Objects; -import java.util.Optional; - -import javax.annotation.Nonnull; -import javax.annotation.ParametersAreNonnullByDefault; -import javax.annotation.ParametersAreNullableByDefault; -import javax.validation.Valid; - import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.MoreObjects; @@ -32,6 +23,14 @@ import io.divolte.server.HttpSource; import io.divolte.server.IncomingRequestProcessingPool; +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; +import javax.annotation.ParametersAreNullableByDefault; +import javax.validation.Valid; +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; + @ParametersAreNonnullByDefault public class BrowserSourceConfiguration extends SourceConfiguration { private static final String DEFAULT_PARTY_COOKIE = "_dvp"; @@ -43,19 +42,21 @@ public class BrowserSourceConfiguration extends SourceConfiguration { private static final String DEFAULT_HTTP_RESPONSE_DELAY = "0 seconds"; public static final BrowserSourceConfiguration DEFAULT_BROWSER_SOURCE_CONFIGURATION = new BrowserSourceConfiguration( - DEFAULT_PREFIX, - DEFAULT_EVENT_SUFFIX, - Optional.empty(), - DEFAULT_PARTY_COOKIE, - DurationDeserializer.parseDuration(DEFAULT_PARTY_TIMEOUT), - DEFAULT_SESSION_COOKIE, - DurationDeserializer.parseDuration(DEFAULT_SESSION_TIMEOUT), - DurationDeserializer.parseDuration(DEFAULT_HTTP_RESPONSE_DELAY), - JavascriptConfiguration.DEFAULT_JAVASCRIPT_CONFIGURATION); + DEFAULT_PREFIX, + DEFAULT_EVENT_SUFFIX, + Optional.empty(), + DEFAULT_PARTY_COOKIE, + DurationDeserializer.parseDuration(DEFAULT_PARTY_TIMEOUT), + DEFAULT_SESSION_COOKIE, + DurationDeserializer.parseDuration(DEFAULT_SESSION_TIMEOUT), + DurationDeserializer.parseDuration(DEFAULT_HTTP_RESPONSE_DELAY), + JavascriptConfiguration.DEFAULT_JAVASCRIPT_CONFIGURATION, + Optional.empty()); public final String prefix; public final String eventSuffix; public final Optional cookieDomain; + public final Optional cookieSameSite; public final String partyCookie; public final Duration partyTimeout; public final String sessionCookie; @@ -67,15 +68,16 @@ public class BrowserSourceConfiguration extends SourceConfiguration { @JsonCreator @ParametersAreNullableByDefault - BrowserSourceConfiguration(@JsonProperty(defaultValue=DEFAULT_PREFIX) final String prefix, - @JsonProperty(defaultValue=DEFAULT_EVENT_SUFFIX) final String eventSuffix, + BrowserSourceConfiguration(@JsonProperty(defaultValue = DEFAULT_PREFIX) final String prefix, + @JsonProperty(defaultValue = DEFAULT_EVENT_SUFFIX) final String eventSuffix, @Nonnull final Optional cookieDomain, - @JsonProperty(defaultValue=DEFAULT_PARTY_COOKIE) final String partyCookie, - @JsonProperty(defaultValue=DEFAULT_PARTY_TIMEOUT) final Duration partyTimeout, - @JsonProperty(defaultValue=DEFAULT_SESSION_COOKIE) final String sessionCookie, - @JsonProperty(defaultValue=DEFAULT_SESSION_TIMEOUT) final Duration sessionTimeout, - @JsonProperty(defaultValue=DEFAULT_HTTP_RESPONSE_DELAY) final Duration httpResponseDelay, - final JavascriptConfiguration javascript) { + @JsonProperty(defaultValue = DEFAULT_PARTY_COOKIE) final String partyCookie, + @JsonProperty(defaultValue = DEFAULT_PARTY_TIMEOUT) final Duration partyTimeout, + @JsonProperty(defaultValue = DEFAULT_SESSION_COOKIE) final String sessionCookie, + @JsonProperty(defaultValue = DEFAULT_SESSION_TIMEOUT) final Duration sessionTimeout, + @JsonProperty(defaultValue = DEFAULT_HTTP_RESPONSE_DELAY) final Duration httpResponseDelay, + final JavascriptConfiguration javascript, + @Nonnull final Optional cookieSameSite) { // TODO: register a custom deserializer with Jackson that uses the defaultValue property from the annotation to fix this this.prefix = Optional.ofNullable(prefix).map(BrowserSourceConfiguration::ensureTrailingSlash).orElse(DEFAULT_PREFIX); this.eventSuffix = Optional.ofNullable(eventSuffix).orElse(DEFAULT_EVENT_SUFFIX); @@ -86,6 +88,7 @@ public class BrowserSourceConfiguration extends SourceConfiguration { this.sessionTimeout = Optional.ofNullable(sessionTimeout).orElseGet(() -> DurationDeserializer.parseDuration(DEFAULT_SESSION_TIMEOUT)); this.httpResponseDelay = Optional.ofNullable(httpResponseDelay).orElseGet(() -> DurationDeserializer.parseDuration(DEFAULT_HTTP_RESPONSE_DELAY)); this.javascript = Optional.ofNullable(javascript).orElse(JavascriptConfiguration.DEFAULT_JAVASCRIPT_CONFIGURATION); + this.cookieSameSite = Objects.requireNonNull(cookieSameSite); } private static String ensureTrailingSlash(final String s) { @@ -95,22 +98,23 @@ private static String ensureTrailingSlash(final String s) { @Override protected MoreObjects.ToStringHelper toStringHelper() { return super.toStringHelper() - .add("prefix", prefix) - .add("eventSuffix", eventSuffix) - .add("cookieDomain", cookieDomain) - .add("partyCookie", partyCookie) - .add("partyTimeout", partyTimeout) - .add("sessionCookie", sessionCookie) - .add("sessionTimeout", sessionTimeout) - .add("httpResponseDelay", httpResponseDelay) - .add("javascript", javascript); + .add("prefix", prefix) + .add("eventSuffix", eventSuffix) + .add("cookieDomain", cookieDomain) + .add("partyCookie", partyCookie) + .add("partyTimeout", partyTimeout) + .add("sessionCookie", sessionCookie) + .add("sessionTimeout", sessionTimeout) + .add("httpResponseDelay", httpResponseDelay) + .add("javascript", javascript) + .add("SameSite", cookieSameSite); } @Override public HttpSource createSource( - final ValidatedConfiguration vc, - final String sourceName, - final IncomingRequestProcessingPool processingPool) { + final ValidatedConfiguration vc, + final String sourceName, + final IncomingRequestProcessingPool processingPool) { return new BrowserSource(vc, sourceName, processingPool); } } diff --git a/src/main/java/io/divolte/server/js/TrackingJavaScriptResource.java b/src/main/java/io/divolte/server/js/TrackingJavaScriptResource.java index 9a777cb7..220d10be 100644 --- a/src/main/java/io/divolte/server/js/TrackingJavaScriptResource.java +++ b/src/main/java/io/divolte/server/js/TrackingJavaScriptResource.java @@ -51,13 +51,14 @@ private static ImmutableMap createScriptConstants(final BrowserS builder.put("PARTY_ID_TIMEOUT_SECONDS", trimLongToMaxInt(browserSourceConfiguration.partyTimeout.get(ChronoUnit.SECONDS))); builder.put("SESSION_COOKIE_NAME", browserSourceConfiguration.sessionCookie); builder.put("SESSION_ID_TIMEOUT_SECONDS", trimLongToMaxInt(browserSourceConfiguration.sessionTimeout.get(ChronoUnit.SECONDS))); - browserSourceConfiguration.cookieDomain.ifPresent((v) -> builder.put("COOKIE_DOMAIN", v)); + browserSourceConfiguration.cookieDomain.ifPresent(v -> builder.put("COOKIE_DOMAIN", v)); + browserSourceConfiguration.cookieSameSite.ifPresent(v -> builder.put("COOKIE_SAME_SITE", v)); builder.put("LOGGING", browserSourceConfiguration.javascript.logging); builder.put(SCRIPT_CONSTANT_NAME, browserSourceConfiguration.javascript.name); builder.put("EVENT_SUFFIX", browserSourceConfiguration.eventSuffix); builder.put("AUTO_PAGE_VIEW_EVENT", browserSourceConfiguration.javascript.autoPageViewEvent); builder.put("EVENT_TIMEOUT_SECONDS", browserSourceConfiguration.javascript.eventTimeout.getSeconds() + - browserSourceConfiguration.javascript.eventTimeout.getNano() / (double)NANOS_PER_SECOND); + browserSourceConfiguration.javascript.eventTimeout.getNano() / (double) NANOS_PER_SECOND); return builder.build(); } @@ -68,7 +69,7 @@ private static int trimLongToMaxInt(long duration) { } else { result = Integer.MAX_VALUE; logger.warn("Configured duration ({}) is too higher; capping at {}.", - duration, result); + duration, result); } return result; } @@ -76,9 +77,9 @@ private static int trimLongToMaxInt(long duration) { public static TrackingJavaScriptResource create(final ValidatedConfiguration vc, final String sourceName) throws IOException { final BrowserSourceConfiguration browserSourceConfiguration = - vc.configuration().getSourceConfiguration(sourceName, BrowserSourceConfiguration.class); + vc.configuration().getSourceConfiguration(sourceName, BrowserSourceConfiguration.class); return new TrackingJavaScriptResource("divolte.js", - createScriptConstants(browserSourceConfiguration), - browserSourceConfiguration.javascript.debug); + createScriptConstants(browserSourceConfiguration), + browserSourceConfiguration.javascript.debug); } } diff --git a/src/main/resources/divolte.js b/src/main/resources/divolte.js index 31278922..e7465ea4 100644 --- a/src/main/resources/divolte.js +++ b/src/main/resources/divolte.js @@ -37,6 +37,8 @@ var SCRIPT_NAME = 'divolte.js'; var EVENT_SUFFIX = 'csc-event'; /** @define {boolean} */ var AUTO_PAGE_VIEW_EVENT = true; +/** @define {string} */ +var COOKIE_SAME_SITE = ''; (function (global, factory) { factory(global); @@ -179,7 +181,7 @@ var AUTO_PAGE_VIEW_EVENT = true; * @param {number} nowMs The current time, in milliseconds since the Unix epoch. * @param {string} domain The domain to set the cookies for, if non-zero in length. */ - var setCookie = function(name, value, maxAgeSeconds, nowMs, domain) { + var setCookie = function(name, value, maxAgeSeconds, nowMs, domain, sameSite) { var expiry = new Date(nowMs + 1000 * maxAgeSeconds); // Assumes cookie name and value are sensible. (For our use they are.) // Note: No domain means these are always first-party cookies. @@ -187,6 +189,12 @@ var AUTO_PAGE_VIEW_EVENT = true; if (domain) { cookieString += "; domain=" + domain; } + + // SameSite supports None; Secure, Strict & Lax + if (sameSite) { + cookieString += "; SameSite=" + sameSite; + } + document.cookie = cookieString; }; @@ -1309,8 +1317,8 @@ var AUTO_PAGE_VIEW_EVENT = true; isFirstInSession = false; // Update the party and session cookies. - setCookie(SESSION_COOKIE_NAME, sessionId, SESSION_ID_TIMEOUT_SECONDS, eventTime, COOKIE_DOMAIN); - setCookie(PARTY_COOKIE_NAME, partyId, PARTY_ID_TIMEOUT_SECONDS, eventTime, COOKIE_DOMAIN); + setCookie(SESSION_COOKIE_NAME, sessionId, SESSION_ID_TIMEOUT_SECONDS, eventTime, COOKIE_DOMAIN, COOKIE_SAME_SITE); + setCookie(PARTY_COOKIE_NAME, partyId, PARTY_ID_TIMEOUT_SECONDS, eventTime, COOKIE_DOMAIN, COOKIE_SAME_SITE); // Last thing we do: add a checksum to the queryString. addParam('x', calculateChecksum(params).toString(36));