play.filters.cors.CORSConfig.scala Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) from 2022 The Play Framework Contributors , 2011-2021 Lightbend Inc.
*/
package play.filters.cors
import scala.concurrent.duration._
import play.api.Configuration
import play.filters.cors.CORSConfig.Origins
/**
* Configuration for play.filters.cors.AbstractCORSPolicy.
*
*
* - allow only requests with origins from a whitelist (by default all origins are allowed)
* - allow only HTTP methods from a whitelist for preflight requests (by default all methods are allowed)
* - allow only HTTP headers from a whitelist for preflight requests (by default all headers are allowed)
* - set custom HTTP headers to be exposed in the response (by default no headers are exposed)
* - disable/enable support for credentials (by default credentials support is enabled)
* - set how long (in seconds) the results of a preflight request can be cached in a preflight result cache (by default 3600 seconds, 1 hour)
* - enable/disable serving requests with origins not in whitelist as non-CORS requests (by default they are forbidden)
*
*
* @param allowedOrigins
* [[http://www.w3.org/TR/cors/#resource-requests §6.1.2]]
* [[http://www.w3.org/TR/cors/#resource-preflight-requests §6.2.2]]
* Always matching is acceptable since the list of origins can be unbounded.
* @param isHttpMethodAllowed
* [[http://www.w3.org/TR/cors/#resource-preflight-requests §6.2.5]]
* Always matching is acceptable since the list of methods can be unbounded.
* @param isHttpHeaderAllowed
* [[http://www.w3.org/TR/cors/#resource-preflight-requests §6.2.6]]
* Always matching is acceptable since the list of headers can be unbounded.
* @param exposedHeaders
* [[https://www.w3.org/TR/cors/#list-of-exposed-headers §6.1.4]]
* By not adding the appropriate headers resource can also clear the preflight result cache of all entries
* where origin is a case-sensitive match for the value of the Origin header and url is a case-sensitive match for the URL of the resource.
* @param supportsCredentials
* [[https://www.w3.org/TR/cors/#supports-credentials §6.1.3]]
* The string "*" cannot be used for a resource that supports credentials.
* @param preflightMaxAge
* [[http://www.w3.org/TR/cors/#resource-preflight-requests §6.2.8]]
* Set how long the user agent is allowed to cache the result of the preflight request.
* @param serveForbiddenOrigins
* Enable/disable serving requests with origins not in whitelist as non-CORS requests.
*/
case class CORSConfig(
allowedOrigins: Origins = Origins.None,
isHttpMethodAllowed: String => Boolean = _ => true,
isHttpHeaderAllowed: String => Boolean = _ => true,
exposedHeaders: Seq[String] = Seq.empty,
supportsCredentials: Boolean = true,
preflightMaxAge: Duration = 1.hour,
serveForbiddenOrigins: Boolean = false
) {
def anyOriginAllowed: Boolean = allowedOrigins == Origins.All
def withAnyOriginAllowed: CORSConfig = withOriginsAllowed(Origins.All)
def withOriginsAllowed(origins: String => Boolean): CORSConfig = copy(allowedOrigins = Origins.Matching(origins))
private[play] def allowedForOrigin(origin: String): Option[String] = allowedOrigins match {
case Origins.All if supportsCredentials => Some(origin)
case Origins.All => Some("*")
case Origins.Matching(fn) if fn(origin) => Some(origin)
case Origins.Matching(fn) if fn("*") => Some("*")
case _ => None
}
def withMethodsAllowed(methods: String => Boolean): CORSConfig = copy(isHttpMethodAllowed = methods)
def withHeadersAllowed(headers: String => Boolean): CORSConfig = copy(isHttpHeaderAllowed = headers)
def withExposedHeaders(headers: Seq[String]): CORSConfig = copy(exposedHeaders = headers)
def withCredentialsSupport(supportsCredentials: Boolean): CORSConfig = copy(supportsCredentials = supportsCredentials)
def withPreflightMaxAge(maxAge: Duration): CORSConfig = copy(preflightMaxAge = maxAge)
def withServeForbiddenOrigins(serveForbiddenOrigins: Boolean): CORSConfig =
copy(serveForbiddenOrigins = serveForbiddenOrigins)
import java.util.{ function => juf }
import scala.jdk.CollectionConverters._
import scala.jdk.FunctionConverters._
def withOriginsAllowed(origins: juf.Function[String, Boolean]): CORSConfig = withOriginsAllowed(origins.asScala)
def withMethodsAllowed(methods: juf.Function[String, Boolean]): CORSConfig = withMethodsAllowed(methods.asScala)
def withHeadersAllowed(headers: juf.Function[String, Boolean]): CORSConfig = withHeadersAllowed(headers.asScala)
def withExposedHeaders(headers: java.util.List[String]): CORSConfig = withExposedHeaders(headers.asScala.toSeq)
def withPreflightMaxAge(maxAge: java.time.Duration): CORSConfig =
withPreflightMaxAge(Duration.fromNanos(maxAge.toNanos))
}
/**
* Helpers to build CORS policy configurations
*/
object CORSConfig {
/**
* Origins allowed by the CORS filter
*/
sealed trait Origins extends (String => Boolean)
object Origins {
case object All extends Origins {
override def apply(v: String) = true
}
case class Matching(func: String => Boolean) extends Origins {
override def apply(v: String) = func(v)
}
val None = Matching(_ => false)
}
/**
*/
val denyAll: CORSConfig =
CORSConfig(
allowedOrigins = Origins.None,
isHttpMethodAllowed = _ => false,
isHttpHeaderAllowed = _ => false,
exposedHeaders = Seq.empty,
supportsCredentials = true,
preflightMaxAge = 0.seconds,
serveForbiddenOrigins = false
)
/**
* Builds a [[CORSConfig]] from a play.api.Configuration instance.
*
* @example The configuration is as follows:
* {{{
* play.filters.cors {
* pathPrefixes = ["/myresource", ...] # ["/"] by default
* allowedOrigins = ["http://...", ...] # If null, all origins are allowed
* allowedHttpMethods = ["PATCH", ...] # If null, all methods are allowed
* allowedHttpHeaders = ["Custom-Header", ...] # If null, all headers are allowed
* exposedHeaders = [...] # empty by default
* supportsCredentials = true # true by default
* preflightMaxAge = 1 hour # 1 hour by default
* serveForbiddenOrigins = false # false by default
* }
*
* }}}
*/
def fromConfiguration(conf: Configuration): CORSConfig = {
val config = conf.get[Configuration]("play.filters.cors")
fromUnprefixedConfiguration(config)
}
private[cors] def fromUnprefixedConfiguration(config: Configuration): CORSConfig = {
CORSConfig(
allowedOrigins = config.get[Option[Seq[String]]]("allowedOrigins") match {
case Some(allowed) => Origins.Matching(allowed.toSet)
case None => Origins.All
},
isHttpMethodAllowed = config
.get[Option[Seq[String]]]("allowedHttpMethods")
.map { methods =>
val s = methods.toSet
s.contains _
}
.getOrElse(_ => true),
isHttpHeaderAllowed = config
.get[Option[Seq[String]]]("allowedHttpHeaders")
.map { headers =>
val s = headers.map(_.toLowerCase(java.util.Locale.ENGLISH)).toSet
s.contains _
}
.getOrElse(_ => true),
exposedHeaders = config.get[Seq[String]]("exposedHeaders"),
supportsCredentials = config.get[Boolean]("supportsCredentials"),
preflightMaxAge = config.get[Duration]("preflightMaxAge"),
serveForbiddenOrigins = config.get[Boolean]("serveForbiddenOrigins")
)
}
}