Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

package akka.http.javadsl.model.headers;

import akka.annotation.DoNotInherit;
import akka.http.javadsl.model.DateTime;
import akka.http.impl.util.Util;
import scala.compat.java8.OptionConverters;

import java.util.Optional;
import java.util.OptionalLong;

@DoNotInherit
public abstract class HttpCookie {
public abstract String name();
public abstract String value();
Expand All @@ -23,22 +25,29 @@ public abstract class HttpCookie {
public abstract boolean secure();
public abstract boolean httpOnly();
public abstract Optional<String> getExtension();
public abstract Optional<SameSite> getSameSite();

public static HttpCookie create(String name, String value) {
return new akka.http.scaladsl.model.headers.HttpCookie(
name, value,
Util.<akka.http.scaladsl.model.DateTime>scalaNone(), Util.scalaNone(), Util.<String>scalaNone(), Util.<String>scalaNone(),
false, false,
Util.<String>scalaNone());
Util.<String>scalaNone(),
Util.<akka.http.scaladsl.model.headers.SameSite>scalaNone());
}
public static HttpCookie create(String name, String value, Optional<String> domain, Optional<String> path) {
return new akka.http.scaladsl.model.headers.HttpCookie(
name, value,
Util.<akka.http.scaladsl.model.DateTime>scalaNone(), Util.scalaNone(),
OptionConverters.toScala(domain), OptionConverters.toScala(path),
false, false,
Util.<String>scalaNone());
Util.<String>scalaNone(),
Util.<akka.http.scaladsl.model.headers.SameSite>scalaNone());
}

/**
* @deprecated Since 10.2.0. Use {@link #create(String, String, Optional, OptionalLong, Optional, Optional, boolean, boolean, Optional, Optional)} instead.
*/
@SuppressWarnings("unchecked")
public static HttpCookie create(
String name,
Expand All @@ -58,7 +67,31 @@ public static HttpCookie create(
OptionConverters.toScala(path),
secure,
httpOnly,
OptionConverters.toScala(extension));
OptionConverters.toScala(extension),
Util.<akka.http.scaladsl.model.headers.SameSite>scalaNone());
}
@SuppressWarnings("unchecked")
public static HttpCookie create(
String name,
String value,
Optional<DateTime> expires,
OptionalLong maxAge,
Optional<String> domain,
Optional<String> path,
boolean secure,
boolean httpOnly,
Optional<String> extension,
Optional<SameSite> sameSite) {
return new akka.http.scaladsl.model.headers.HttpCookie(
name, value,
Util.<DateTime, akka.http.scaladsl.model.DateTime>convertOptionalToScala(expires),
OptionConverters.toScala(maxAge),
OptionConverters.toScala(domain),
OptionConverters.toScala(path),
secure,
httpOnly,
OptionConverters.toScala(extension),
OptionConverters.toScala(sameSite.map(SameSite::asScala)));
}

/**
Expand Down Expand Up @@ -91,6 +124,16 @@ public static HttpCookie create(
*/
public abstract HttpCookie withHttpOnly(boolean httpOnly);

/**
* Returns a copy of this HttpCookie instance with the given {@link SameSite} set.
*/
public abstract HttpCookie withSameSite(SameSite sameSite);

/**
* Returns a copy of this HttpCookie instance with the given Optional {@link SameSite} set.
*/
public abstract HttpCookie withSameSite(Optional<SameSite> sameSite);

/**
* Returns a copy of this HttpCookie instance with the given extension set.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2020 Lightbend Inc. <https://www.lightbend.com>
*/

package akka.http.javadsl.model.headers;

/**
* The Cookie SameSite attribute as defined by <a href="https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00">RFC6265bis</a>
* and <a href="https://tools.ietf.org/html/draft-west-cookie-incrementalism-00">Incrementally Better Cookies</a>.
*/
public enum SameSite {
Strict,
Lax,
// SameSite.None is different from not adding the SameSite attribute in a cookie.
// - Cookies without a SameSite attribute will be treated as SameSite=Lax.
// - Cookies for cross-site usage must specify `SameSite=None; Secure` to enable inclusion in third party
// context. We are not enforcing `; Secure` when `SameSite=None`, but users should.
None;

public akka.http.scaladsl.model.headers.SameSite asScala() {
if (this == Strict) return akka.http.scaladsl.model.headers.SameSite.Strict$.MODULE$;
if (this == Lax) return akka.http.scaladsl.model.headers.SameSite.Lax$.MODULE$;
return akka.http.scaladsl.model.headers.SameSite.None$.MODULE$;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Add SameSite attribute to HttpCookie

ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.javadsl.model.headers.HttpCookie.getSameSite")
ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.javadsl.model.headers.HttpCookie.withSameSite")

Original file line number Diff line number Diff line change
Expand Up @@ -266,19 +266,19 @@ private[parser] trait CommonRules { this: Parser with StringBuilding =>
}

def `cookie-av` = rule {
`expires-av` | `max-age-av` | `domain-av` | `path-av` | `secure-av` | `httponly-av` | `extension-av`
`expires-av` | `max-age-av` | `domain-av` | `path-av` | `same-site-av` | `secure-av` | `httponly-av` | `extension-av`
}

def `expires-av` = rule {
ignoreCase("expires=") ~ OWS ~ `expires-date` ~> { (c: HttpCookie, dt: DateTime) => c.copy(expires = Some(dt)) }
ignoreCase("expires=") ~ OWS ~ `expires-date` ~> { (c: HttpCookie, dt: DateTime) => c.withExpires(dt) }
}

def `max-age-av` = rule {
ignoreCase("max-age=") ~ OWS ~ longNumberCappedAtIntMaxValue ~> { (c: HttpCookie, seconds: Long) => c.copy(maxAge = Some(seconds)) }
ignoreCase("max-age=") ~ OWS ~ longNumberCappedAtIntMaxValue ~> { (c: HttpCookie, seconds: Long) => c.withMaxAge(seconds) }
}

def `domain-av` = rule {
ignoreCase("domain=") ~ OWS ~ `domain-value` ~> { (c: HttpCookie, domainName: String) => c.copy(domain = Some(domainName)) }
ignoreCase("domain=") ~ OWS ~ `domain-value` ~> { (c: HttpCookie, domainName: String) => c.withDomain(domainName) }
}

// https://tools.ietf.org/html/rfc1034#section-3.5 relaxed by https://tools.ietf.org/html/rfc1123#section-2
Expand All @@ -288,20 +288,28 @@ private[parser] trait CommonRules { this: Parser with StringBuilding =>
}

def `path-av` = rule {
ignoreCase("path=") ~ OWS ~ `path-value` ~> { (c: HttpCookie, pathValue: String) => c.copy(path = Some(pathValue)) }
ignoreCase("path=") ~ OWS ~ `path-value` ~> { (c: HttpCookie, pathValue: String) => c.withPath(pathValue) }
}

// http://www.rfc-editor.org/errata_search.php?rfc=6265
def `path-value` = rule {
capture(zeroOrMore(`av-octet`)) ~ OWS
}

def `same-site-av` = rule {
ignoreCase("samesite=") ~ OWS ~ `same-site-value` ~> { (c: HttpCookie, sameSiteValue: String) => c.withSameSite(sameSite = SameSite(sameSiteValue)) }
}

def `same-site-value` = rule {
capture(ignoreCase("lax") | ignoreCase("strict") | ignoreCase("none")) ~ OWS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means samesite=wrong will fail to parse. Do we want that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be ok I think for the first version. If people run into that problem often enough, we can think about providing alternatives.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, since it is the server-side sending those, only our client-side would be affected where cookie usage currently isn't as important.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you're right. Let's keep it like this then. I'll update the tests - do you think we should call this out in the release/migration notes?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't hurt to mention it. It's not really worse than before, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, let's leave it like this.

}

def `secure-av` = rule {
ignoreCase("secure") ~ OWS ~> { (cookie: HttpCookie) => cookie.copy(secure = true) }
ignoreCase("secure") ~ OWS ~> { (cookie: HttpCookie) => cookie.withSecure(true) }
}

def `httponly-av` = rule {
ignoreCase("httponly") ~ OWS ~> { (cookie: HttpCookie) => cookie.copy(httpOnly = true) }
ignoreCase("httponly") ~ OWS ~> { (cookie: HttpCookie) => cookie.withHttpOnly(true) }
}

// http://www.rfc-editor.org/errata_search.php?rfc=6265
Expand All @@ -310,9 +318,10 @@ private[parser] trait CommonRules { this: Parser with StringBuilding =>
| ignoreCase("max-age=")
| ignoreCase("domain=")
| ignoreCase("path=")
| ignoreCase("samesite=")
| ignoreCase("secure")
| ignoreCase("httponly")) ~
capture(zeroOrMore(`av-octet`)) ~ OWS ~> { (c: HttpCookie, s: String) => c.copy(extension = Some(s)) }
capture(zeroOrMore(`av-octet`)) ~ OWS ~> { (c: HttpCookie, s: String) => c.withExtension(s) }
}

// ******************************************************************************************
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ final class ErrorInfo(

/** INTERNAL API */
@InternalApi private[akka] def this(summary: String, detail: String) = this(summary, detail, "")

override def toString(): String = s"ErrorInfo($summary, $detail, $errorHeaderName)"
}

object ErrorInfo {
Expand Down
Loading