Skip to content

Instantly share code, notes, and snippets.

@ChristopherDavenport
Last active July 17, 2019 04:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ChristopherDavenport/048cd1eb5325af121629fb9dd5131015 to your computer and use it in GitHub Desktop.
Save ChristopherDavenport/048cd1eb5325af121629fb9dd5131015 to your computer and use it in GitHub Desktop.
CookieJar Implementation
import cats._
import cats.implicits._
import cats.effect._
import cats.effect.concurrent._
import org.http4s._
import org.http4s.client.Client
import scala.concurrent.duration._
object CookieJar {
def apply[F[_]: Sync, A](
alg: CookieJarAlg[F]
)(
client: Client[F]
): Client[F] =
Client{req =>
for{
_ <- Resource.liftF(alg.evictExpired)
modRequest <- Resource.liftF(alg.enrichRequest(req))
out <- client.run(modRequest)
_ <- Resource.liftF(out.cookies.traverse_(alg.addCookie))
} yield out
}
trait CookieJarAlg[F[_]]{
def evictExpired: F[Unit]
def evictAll: F[Unit]
def addCookie(c: ResponseCookie): F[Unit]
def enrichRequest[G[_]](r: Request[G]): F[Request[G]]
}
object CookieJarAlg {
def impl[F[_]: Sync: Clock]: F[CookieJarAlg[F]] =
in[F, F]
def in[F[_]: Sync: Clock, G[_]: Sync]: G[CookieJarAlg[F]] =
Ref.in[G, F, Map[CookieKey, CookieValue]](Map.empty).map{ ref =>
new CookieJarAlg[F]{
override def evictExpired: F[Unit] = for {
now <- currentHttpDate
out <- ref.update(
_.filterNot(t => isExpiredByExpiration(now)(t) || isExpiredByMaxAge(now)(t))
)
} yield out
override def evictAll: F[Unit] = ref.set(Map.empty)
override def addCookie(c: ResponseCookie): F[Unit] = for {
now <- currentHttpDate
out <- ref.update(extractFromResponseCookie(_)(c, now))
} yield out
override def enrichRequest[N[_]](r: Request[N]): F[Request[N]] =
for {
cookies <- ref.get.map(_.map(_._2.cookie).toList)
applicable = cookiesForRequest(r, cookies)
out = applicable.foldLeft(r){ case (req, cookie) => req.addCookie(cookie)}
} yield out
}
}
}
private final case class CookieKey(
name: String,
domain: Option[String],
path: Option[String]
)
private final case class CookieValue(
setAt: HttpDate,
cookie: ResponseCookie
)
private def currentHttpDate[F[_]: Clock: MonadError[?[_], Throwable]] =
Clock[F].monotonic(SECONDS)
.flatMap(s => HttpDate.fromEpochSecond(s).liftTo[F])
private def keyFromRespCookie(c: ResponseCookie): CookieKey =
CookieKey(c.name, c.domain, c.path)
private def extractFromResponseCookie[F[_]](
m: Map[CookieKey,CookieValue]
)(c: ResponseCookie, httpDate: HttpDate): Map[CookieKey, CookieValue] =
m + (keyFromRespCookie(c) -> CookieValue(httpDate, c))
private def isExpiredByExpiration(
now: HttpDate
)(m: (CookieKey, CookieValue)): Boolean =
m._2.cookie.expires.forall(expiresAt => now <= expiresAt)
private def isExpiredByMaxAge(
now: HttpDate
)(m: (CookieKey, CookieValue)): Boolean =
m._2.cookie.maxAge.forall{plusSeconds =>
val epochSecondExpiredAt = m._2.setAt.epochSecond + plusSeconds
now <= HttpDate.unsafeFromEpochSecond(epochSecondExpiredAt)
}
private def responseCookieToRequestCookie(r: ResponseCookie): RequestCookie =
RequestCookie(r.name, r.content)
private def cookieAppliesToRequest[N[_]](r: Request[N], c: ResponseCookie): Boolean =
c.domain.exists(s => r.uri.host.forall(host => host.value.contains(s))) &&
c.path.exists(s => r.pathInfo.contains(s))
private def cookiesForRequest[N[_]](
r: Request[N],
l: List[ResponseCookie]
): List[RequestCookie] = l.foldLeft(List.empty[RequestCookie]){case (list, cookie) =>
if (cookieAppliesToRequest(r, cookie)) responseCookieToRequestCookie(cookie) :: list
else list
}
}
@rossabaker
Copy link

I'm too sleepy to cross-reference the implementation with the RFC, but some quick impressions:

  • There is already a RequestCookieJar in core. I don't think it's particularly useful, and I think we should deprecate it.

  • The suffix Alg has always peeved me. It reminds me of the I prefix. I want to call the algebra the CookieJar, but that leaves the middleware without an obvious name. 🤔

  • Doesn't addCookie need a Uri?

  • Should have a way to extract and bulk load all the cookies to support persistence across sessions.

  • Should be able to invalidate specific cookies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment