defmodule Corsica do
@moduledoc """
[Plug](https://github.com/elixir-plug/plug)-based swiss-army knife for CORS requests.
Corsica provides facilities for dealing with
[CORS](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) requests
and responses. It provides:
* low-level functions that let you decide when and where to deal with CORS
requests and CORS response headers;
* a **plug** that handles CORS requests and responds to preflight requests;
* a **router** that can be used in your modules in order to turn them into CORS
handlers which provide fine control over dealing with CORS requests.
## How It Works
Corsica is compliant with the [W3C CORS
specification](http://www.w3.org/TR/cors/). As per this specification, Corsica
**doesn't put any CORS response headers** in a connection that holds an invalid
CORS request. To know what "invalid" CORS request means, have a look at the
[*Validity of CORS Requests* section](#module-validity-of-cors-requests) below.
> #### Headers or No Headers? {: .warning}
>
> When some options that are not mandatory and have no default value (such as
> `:max_age`) are not passed to Corsica, the relative header will often **not be sent**
> at all. This is compliant with the specification, and at the same time it reduces the size of
> the response, even if just by a handful of bytes.
The following is a list of all the *CORS response headers* supported by Corsica:
* `Access-Control-Allow-Origin`
* `Access-Control-Allow-Methods`
* `Access-Control-Allow-Headers`
* `Access-Control-Allow-Credentials`
* `Access-Control-Allow-Private-Network`
* `Access-Control-Expose-Headers`
* `Access-Control-Max-Age`
* `Vary` (see [the relevant section](#module-the-vary-header) below)
## Options
Corsica supports the following options, in the `use` macro, in
`Corsica.Router.resource/2`, and in the `Corsica` plug.
* `:origins` (`t:origin/0`, list of `t:origin/0`, or the string `"*"`) This option is **required**. The origin of
a request (specified by the `"origin"` request header) will be considered a valid origin
if it "matches" at least one of the origins specified in `:origins`. What
"matches" means depends on the type of origin. See `t:origin/0` for more information.
The value `"*"` can also be used to match every origin and reply with `*` as
the value of the `access-control-allow-origin` header. If `"*"` is used, it
must be used as the only value of `:origins` (that is, it can't be used inside
a list of accepted origins). For example:
# Matches everything.
plug Corsica, origins: "*"
# Matches one of the given origins
plug Corsica, origins: ["http://foo.com", "http://bar.com"]
# Matches the given regex
plug Corsica, origins: ~r{^https?://(.*\.?)foo\.com$}
> #### The Origin Showed to Clients {: .info}
>
> This option directly influences the value of the
> `access-control-allow-origin` response header. When `:origins` is `"*"`, the
> `access-control-allow-origin` header is set to `*` as well. If the request's
> origin is allowed and `:origins` is something different than `"*"`, then you
> won't see that value as the value of the `access-control-allow-origin` header:
> the value of this header will be the request's origin (which is *mirrored*).
> This behaviour is intentional: it's compliant with the W3C CORS specification
> and at the same time it provides the advantage of "hiding" all the allowed
> origins from the client (which only sees its origin as an allowed origin).
* `:allow_methods` (list of `t:String.t/0`, or `:all`) -
This is the list
of methods allowed in the `access-control-request-method` header of preflight
requests. If the method requested by the preflight request is in this list or is
a *simple method* (`HEAD`, `GET`, or `POST`), then that method is always allowed.
The methods specified by this option are returned in the `access-control-allow-methods`
response header. If the value of this option is `:all`, all
request methods are allowed and only the method in `access-control-request-method` is
returned as the value of the `access-control-allow-methods` header. Defaults to `["PUT", "PATCH", "DELETE"]` (which means these methods
are allowed *alongside simple methods*).
* `:allow_headers` (list of `t:String.t/0`, or `:all`) - This is the list
of headers allowed in the `access-control-request-headers` header of preflight
requests. If a header requested by the preflight request is in this list or is a
*simple header*, then that
header is always allowed. These are the simple headers defined in the spec:
* `Accept`
* `Accept-Language`
* `Content-Language`
The headers specified by this option are returned in the
`access-control-allow-headers` response header. If the value of this option is `:all`, all request
headers are allowed and only the headers in `access-control-request-headers` are
returned as the value of the `access-control-allow-headers` header. Defaults to `[]` (which means only
the simple headers are allowed)
* `:allow_credentials` (`t:boolean/0`) - If `true`, sends the
`access-control-allow-credentials` with value `true`. If `false`, prevents
that header from being sent at all. Defaults to `false`.
> #### `Access-Control-Allow-Origin` Header with Credentials {: .info}
>
> If `:origins` is set to `"*"` and
> `:allow_credentials` is set to `true`, then the value of the
> `access-control-allow-origin` header will always be the value of the
> `origin` request header (as per the W3C CORS specification) and not `*`.
* `:allow_private_network` (`t:boolean/0`0 - If `true`, sets the value of the
`access-control-allow-private-network` header used with preflight requests, which
indicates that a resource can be safely shared with external networks. If `false`,
the `access-control-allow-private-network` is not sent at all. Defaults to `false`.
* `:expose_headers` (list of `t:String.t/0`) Sets the value of
the `access-control-expose-headers` response header. This option *does
not* have a default value; if it's not provided, the
`access-control-expose-headers` header is not sent at all.
* `:max_age` (`t:String.t/0` or `t:non_neg_integer/0`) Sets the value of the
`access-control-max-age` header used with preflight requests. This option
*does not* have a default value; if it's not provided, the
`access-control-max-age` header is not sent at all.
* `:telemetry_metadata` (`t:map/0`) - *extra* telemetry metadata to be included in all emitted
events. This can be useful for identifying which `plug Corsica` call is emitting
the events. See `Corsica.Telemetry` for more information on Telemetry in Corsica.
Available since v2.0.0.
* `:passthrough_non_cors_requests` (`t:boolean/0`) - If `true`, allows
non-CORS requests to pass through the plug. See `cors_req?/1` and
`preflight_req?/1` to understand what constitutes a CORS request. What we
mean by "allowing non-CORS requests" means that Corsica won't verify the
`Origin` header and such, but will still add CORS headers to the response.
Defaults to `false`. Available since v2.1.0.
To recap which headers are sent based on options, here's a handy table:
| Header | Request Type | Presence in the Response |
|----------------------------------------|-------------------|--------------------------------|
| `access-control-allow-origin` | simple, preflight | always |
| `access-control-allow-headers` | preflight | always |
| `access-control-allow-credentials` | preflight | `allow_credentials: true` |
| `access-control-allow-private-network` | preflight | `allow_private_network: true` |
| `access-control-expose-headers` | preflight | `:expose_headers` is not empty |
| `access-control-max-age` | preflight | `:max_age` is present |
## Usage
You can use Corsica as a plug or as a router.
### Using Corsica as a Plug
When `Corsica` is used as a plug, it intercepts **all requests**. It only sets a
bunch of CORS headers for regular CORS requests, but it responds (with a `200 OK`
and the appropriate headers) to preflight requests.
If you want to use `Corsica` as a plug, be sure to plug it in your plug
pipeline **before** any router-like plug: routers like `Plug.Router` (or
`Phoenix.Router`) respond to HTTP verbs as well as request URLs, so if
`Corsica` is plugged after a router then preflight requests (which are
`OPTIONS` requests), that will often result in 404 errors since no route responds to
them. Router-like plugs also include plugs like `Plug.Static`, which
respond to requests and halt the pipeline.
defmodule MyApp.Endpoint do
plug Head
plug Corsica, max_age: 600, origins: "*", expose_headers: ~w(X-Foo)
plug Plug.Static
plug MyApp.Router
end
### Using Corsica as a Router Generator
When `Corsica` is used as a plug, it doesn't provide control over which urls
are CORS-enabled or with which options. In order to do that, you can use
`Corsica.Router`. See the documentation for `Corsica.Router` for more
information.
## The `vary` Header
When Corsica is configured such that the `access-control-allow-origin` response
header will vary depending on the `origin` request header, then a `vary: origin`
response header will be set.
## Responding to Preflight Requests
When the request is a preflight request and a valid one (valid origin, valid
request method, and valid request headers), Corsica directly sends a response
to that request instead of just adding headers to the connection (so that a
possible plug pipeline can continue). To do this, Corsica **halts the
connection** (through `Plug.Conn.halt/1`) and **sends a response**.
## Validity of CORS Requests
"Invalid CORS request" can mean that a request doesn't have an `Origin` header
(so it's not a CORS request at all) or that it's a CORS request but:
* the `Origin` request header doesn't match any of the allowed origins
* the request is a preflight request but it requests to use a method or
some headers that are not allowed (via the `Access-Control-Request-Method`
and `Access-Control-Request-Headers` headers)
## Telemetry
Corsica emits some [telemetry](https://github.com/beam-telemetry/telemetry) events.
See `Corsica.Telemetry` for documentation.
## Logging
Corsica used to support `Logger` logging through the `:log` option. This option
has been removed in v2.0.0 in favor of Telemetry events. If you want to keep the
logging behavior, see `Corsica.Telemetry.attach_default_handler/1`.
"""
# Here are some nice (and apparently overlooked!) quotes from the W3C CORS
# specification, along with some thoughts around them.
#
# http://www.w3.org/TR/cors/#access-control-allow-credentials-response-header
# The syntax of the Access-Control-Allow-Credentials header only accepts the
# value "true" (without quotes, case-sensitive). Any other value is not
# conforming to the official CORS specification (many libraries tend to just
# shove the value of a boolean in that header, so it happens to have the value
# "false" as well).
#
# http://www.w3.org/TR/cors/#resource-requests, item 3.
# > The string "*" cannot be used [as the value for the
# > Access-Control-Allow-Origin] header for a resource that supports
# > credentials.
#
# http://www.w3.org/TR/cors/#resource-implementation
# > [...] [authors] should send a Vary: Origin HTTP header or provide other
# > appropriate control directives to prevent caching of such responses, which
# > may be inaccurate if re-used across-origins.
#
# http://www.w3.org/TR/cors/#resource-preflight-requests, item 9.
# > If method is a simple method, [setting the Access-Control-Allow-Methods
# > header] may be skipped (but it is not prohibited).
# > Simply returning the method indicated by Access-Control-Request-Method (if
# > supported) can be enough.
# However, this behaviour can inhibit caching from the client side since the
# client has to issue a preflight request for each method it wants to use,
# while if all the allowed methods are returned every time then the cached
# preflight request can be used more times.
#
# http://www.w3.org/TR/cors/#resource-preflight-requests, item 10.
# > If each of the header field names is a simple header and none is
# > Content-Type, [setting the Access-Control-Allow-Headers] may be
# > skipped. Simply returning supported headers from
# > Access-Control-Allow-Headers can be enough.
# The same argument for Access-Control-Allow-Methods can be made here.
import Plug.Conn
alias Plug.Conn
@behaviour Plug
defmodule Options do
@moduledoc false
defstruct [
:max_age,
:expose_headers,
:origins,
allow_methods: ~w(PUT PATCH DELETE),
allow_headers: [],
allow_credentials: false,
allow_private_network: false,
passthrough_non_cors_requests: false,
telemetry_metadata: %{}
]
end
@typedoc """
An origin that can be specified in the `:origins` option.
This is how each type of origin is used in order to check for "matching" origins:
* strings - the actual origin and the allowed origin have to be identical
* regexes - the actual origin has to match the allowed regex (as per `Regex.match?/2`)
* `{module, function, args}` tuples - `module.function` is called with
two extra arguments prepended to the given `args`: the current connection
and the actual origin; if it returns `true` the origin is accepted,
if it returns `false` the origin is not accepted.
"""
@typedoc since: "2.0.0"
@type origin() :: String.t() | Regex.t() | {module(), function :: atom(), args :: [term()]}
@typedoc """
Options accepted by most functions as well as the `Corsica` plug.
The `%Options{}` struct is internal to Corsica and is used for performance.
"""
@typedoc since: "2.1.0"
@type options() :: keyword() | %Options{}
@simple_methods ~w(GET HEAD POST)
@simple_headers ~w(accept accept-language content-language)
# Plug callbacks.
@impl Plug
def init(opts) do
sanitize_opts(opts)
end
@impl Plug
def call(%Conn{} = conn, %Options{} = opts) do
cond do
opts.passthrough_non_cors_requests and conn.method == "OPTIONS" ->
send_preflight_resp(conn, opts)
opts.passthrough_non_cors_requests ->
put_cors_simple_resp_headers(conn, opts)
not cors_req?(conn) ->
conn
not preflight_req?(conn) ->
put_cors_simple_resp_headers(conn, opts)
true ->
send_preflight_resp(conn, opts)
end
end
# Public so that it can be called from `Corsica.Router` (and for testing too).
@doc false
def sanitize_opts(opts) when is_list(opts) do
opts
|> require_origins_option()
|> to_options_struct()
|> Map.update!(:allow_methods, fn
:all -> :all
methods -> Enum.map(methods, &String.upcase/1)
end)
|> Map.update!(:allow_headers, fn
:all -> :all
headers -> Enum.map(headers, &String.downcase/1)
end)
|> maybe_update_option(:max_age, &to_string/1)
|> maybe_update_option(:expose_headers, &Enum.join(&1, ","))
|> maybe_warn_tuple_origins()
|> maybe_warn_passthrough_non_cors_requests_option()
end
defp to_options_struct(opts), do: struct(Options, opts)
defp require_origins_option(opts) do
if not Keyword.has_key?(opts, :origins) do
raise ArgumentError, "the :origins option is required"
end
opts
end
defp maybe_update_option(opts, option, update_fun) do
if value = Map.get(opts, option) do
Map.put(opts, option, update_fun.(value))
else
opts
end
end
defp maybe_warn_tuple_origins(%{origins: origins} = opts) do
for {_module, _function} = origin <- List.wrap(origins) do
IO.warn(
"passing #{inspect(origin)} as an allowed origin is deprecated, " <>
"please see {module, function, args} for an alternative"
)
end
opts
end
defp maybe_warn_passthrough_non_cors_requests_option(opts) do
if opts.passthrough_non_cors_requests and opts.origins != "*" do
IO.warn(
"if the :passthrough_non_cors_requests option is set to true, " <>
"then you need to set the :origins option to \"*\""
)
end
opts
end
# Utilities
@doc """
Checks whether a given connection holds a CORS request.
This function doesn't check if the CORS request is a *valid* CORS request: it
just checks that it's a CORS request, that is, it has an `Origin` request
header.
"""
@spec cors_req?(Conn.t()) :: boolean
def cors_req?(%Conn{} = conn), do: get_req_header(conn, "origin") != []
@doc """
Checks whether a given connection holds a preflight CORS request.
This function doesn't check that the preflight request is a *valid* CORS
request: it just checks that it's a preflight request. A request is considered
to be a CORS preflight request if and only if its request method is `OPTIONS`
and it has a `Access-Control-Request-Method` request header.
Note that if a request is a valid preflight request, that makes it a valid
CORS request as well. You can thus call just `preflight_req?/1` instead of
`preflight_req?/1` and `cors_req?/1`.
"""
@spec preflight_req?(Conn.t()) :: boolean
def preflight_req?(%Conn{method: "OPTIONS"} = conn),
do: cors_req?(conn) and get_req_header(conn, "access-control-request-method") != []
def preflight_req?(%Conn{}), do: false
# Request handling
@doc """
Sends a CORS preflight response regardless of the request being a valid CORS
request or not.
This function assumes nothing about `conn`. If it's a valid CORS preflight
request with an allowed origin, CORS headers are set by calling
`put_cors_preflight_resp_headers/2` and the response **is sent** with status
`status` and body `body`. `conn` is **halted** before being sent.
The response is always sent because if the request is not a valid CORS
request, then no CORS headers will be added to the response. This behaviour
will be interpreted by the browser as a non-allowed preflight request, as
expected.
For more information on what headers are sent with the response if the
preflight request is valid, look at the documentation for
`put_cors_preflight_resp_headers/2`.
## Options
This function accepts the same options accepted by the `Corsica` plug
(described in the documentation for the `Corsica` module).
## Examples
This function could be used to manually build a plug that responds to
preflight requests. For example:
defmodule MyRouter do
use Plug.Router
plug :match
plug :dispatch
options "/foo",
do: Corsica.send_preflight_resp(conn, origins: "*")
get "/foo",
do: send_resp(conn, 200, "ok")
end
"""
@spec send_preflight_resp(Conn.t(), 100..599, binary(), options()) :: Conn.t()
def send_preflight_resp(conn, status \\ 200, body \\ "", opts)
def send_preflight_resp(%Conn{} = conn, status, body, opts) when is_list(opts) do
send_preflight_resp(conn, status, body, sanitize_opts(opts))
end
def send_preflight_resp(%Conn{} = conn, status, body, %Options{} = opts) do
conn
|> put_cors_preflight_resp_headers(opts)
|> halt()
|> send_resp(status, body)
end
@doc """
Adds CORS response headers to a simple CORS request to `conn`.
This function assumes nothing about `conn`. If `conn` holds an invalid CORS
request or a request whose origin is not allowed, `conn` is returned
unchanged; the absence of CORS headers will be interpreted as an invalid CORS
response by the browser (according to the W3C spec).
If the CORS request is valid, the following response headers are set:
* `Access-Control-Allow-Origin`
and the following headers are optionally set (if the corresponding option is
present):
* `Access-Control-Expose-Headers` (if the `:expose_headers` option is
present)
* `Access-Control-Allow-Credentials` (if the `:allow_credentials` option is
`true`)
## Options
This function accepts the same options accepted by the `Corsica` plug
(described in the documentation for the `Corsica` module).
## Examples
conn
|> put_cors_simple_resp_headers(origins: "*", allow_credentials: true)
|> send_resp(200, "Hello!")
"""
@spec put_cors_simple_resp_headers(Conn.t(), options()) :: Conn.t()
def put_cors_simple_resp_headers(conn, opts)
def put_cors_simple_resp_headers(%Conn{} = conn, opts) when is_list(opts) do
put_cors_simple_resp_headers(conn, sanitize_opts(opts))
end
def put_cors_simple_resp_headers(%Conn{} = conn, %Options{} = opts) do
cond do
opts.passthrough_non_cors_requests ->
execute_telemetry(conn, opts, [:accepted_request], %{request_type: :simple})
conn
|> put_common_headers(opts)
|> put_expose_headers_header(opts)
not cors_req?(conn) ->
execute_telemetry(conn, opts, [:invalid_request], %{request_type: :simple})
conn
not allowed_origin?(conn, opts) ->
execute_telemetry(conn, opts, [:rejected_request], %{request_type: :simple})
conn
true ->
execute_telemetry(conn, opts, [:accepted_request], %{request_type: :simple})
conn
|> put_common_headers(opts)
|> put_expose_headers_header(opts)
end
end
@doc """
Adds CORS response headers to a preflight request to `conn`.
This function assumes nothing about `conn`. If `conn` holds an invalid CORS
request or an invalid preflight request, then `conn` is returned unchanged;
the absence of CORS headers will be interpreted as an invalid CORS response by
the browser (according to the W3C spec).
If the request is a valid CORS request, the following headers will be added to
the response:
* `Access-Control-Allow-Origin`
* `Access-Control-Allow-Methods`
* `Access-Control-Allow-Headers`
and the following headers will optionally be added (based on the value of the
corresponding options):
* `Access-Control-Allow-Credentials` (if the `:allow_credentials` option is
`true`)
* `Access-Control-Allow-Private-Network` (if the `:allow_private_network` option is
`true`)
* `Access-Control-Max-Age` (if the `:max_age` option is present)
## Options
This function accepts the same options accepted by the `Corsica` plug
(described in the documentation for the `Corsica` module).
## Examples
put_cors_preflight_resp_headers conn, [
max_age: 86400,
allow_headers: ~w(X-Header),
allow_private_network: true,
origins: ~r/\w+\.foo\.com$/
]
"""
@spec put_cors_preflight_resp_headers(Conn.t(), options()) :: Conn.t()
def put_cors_preflight_resp_headers(conn, opts)
def put_cors_preflight_resp_headers(%Conn{} = conn, opts) when is_list(opts) do
put_cors_preflight_resp_headers(conn, sanitize_opts(opts))
end
def put_cors_preflight_resp_headers(%Conn{} = conn, %Options{} = opts) do
cond do
opts.passthrough_non_cors_requests ->
execute_telemetry(conn, opts, [:accepted_request], %{request_type: :preflight})
put_cors_preflight_resp_headers_no_check(conn, opts)
not preflight_req?(conn) ->
execute_telemetry(conn, opts, [:invalid_request], %{request_type: :preflight})
conn
not allowed_origin?(conn, opts) ->
execute_telemetry(conn, opts, [:rejected_request], %{
request_type: :preflight,
reason: :origin_not_allowed
})
conn
not allowed_preflight?(conn, opts) ->
# More detailed info is emitted from allowed_preflight?/2.
conn
true ->
execute_telemetry(conn, opts, [:accepted_request], %{request_type: :preflight})
put_cors_preflight_resp_headers_no_check(conn, opts)
end
end
defp put_cors_preflight_resp_headers_no_check(conn, opts) do
conn
|> put_common_headers(opts)
|> put_allow_methods_header(opts)
|> put_allow_headers_header(opts)
|> put_allow_private_network_header(opts)
|> put_max_age_header(opts)
end
defp put_common_headers(conn, %Options{} = opts) do
conn
|> put_allow_credentials_header(opts)
|> put_allow_origin_header(opts)
|> update_vary_header(opts)
end
defp put_allow_credentials_header(conn, %Options{allow_credentials: allow_credentials}) do
if allow_credentials do
put_resp_header(conn, "access-control-allow-credentials", "true")
else
conn
end
end
defp put_allow_origin_header(conn, %Options{passthrough_non_cors_requests: true, origins: "*"}) do
put_resp_header(conn, "access-control-allow-origin", "*")
end
defp put_allow_origin_header(conn, %Options{} = opts) do
[actual_origin | _] = get_req_header(conn, "origin")
value =
if send_wildcard_origin?(opts) do
"*"
else
actual_origin
end
put_resp_header(conn, "access-control-allow-origin", value)
end
# Add `vary: origin` response header if the `access-control-allow-origin` response header will
# vary depending on the `origin` request header.
defp update_vary_header(conn, %Options{origins: [origin]} = opts) do
update_vary_header(conn, %{opts | origins: origin})
end
defp update_vary_header(conn, %Options{origins: origins} = opts) do
cond do
is_binary(origins) and origins != "*" -> conn
send_wildcard_origin?(opts) -> conn
true -> %{conn | resp_headers: [{"vary", "origin"} | conn.resp_headers]}
end
end
defp send_wildcard_origin?(%Options{origins: origins, allow_credentials: allow_credentials}) do
# '*' cannot be used as the value of the `Access-Control-Allow-Origins`
# header if `Access-Control-Allow-Credentials` is true.
origins == "*" and not allow_credentials
end
defp put_allow_methods_header(conn, %Options{allow_methods: allow_methods}) do
value =
if allow_methods == :all do
hd(get_req_header(conn, "access-control-request-method"))
else
Enum.join(allow_methods, ",")
end
put_resp_header(conn, "access-control-allow-methods", value)
end
defp put_allow_headers_header(conn, %Options{allow_headers: allow_headers}) do
allowed_headers =
if allow_headers == :all do
for req_headers <- get_req_header(conn, "access-control-request-headers"),
req_headers = String.downcase(req_headers),
req_header <- Plug.Conn.Utils.list(req_headers),
do: req_header
else
allow_headers
end
put_resp_header(conn, "access-control-allow-headers", Enum.join(allowed_headers, ","))
end
defp put_allow_private_network_header(conn, %Options{allow_private_network: allow?}) do
if allow? do
put_resp_header(conn, "access-control-allow-private-network", "true")
else
conn
end
end
defp put_max_age_header(conn, %Options{max_age: max_age}) do
if max_age do
put_resp_header(conn, "access-control-max-age", max_age)
else
conn
end
end
defp put_expose_headers_header(conn, %Options{expose_headers: expose_headers}) do
if expose_headers && expose_headers != "" do
put_resp_header(conn, "access-control-expose-headers", expose_headers)
else
conn
end
end
# Made public since this function is only called by macros as of now, and so
# an 'unused function' warning is issued if the macros produce no code.
@doc false
def origin(conn) do
conn |> get_req_header("origin") |> List.first()
end
# Made public for testing
@doc false
def allowed_origin?(_conn, %Options{origins: "*"}) do
true
end
def allowed_origin?(conn, %Options{origins: origins}) do
[origin | _] = get_req_header(conn, "origin")
Enum.any?(List.wrap(origins), &matching_origin?(&1, origin, conn))
end
defp matching_origin?(origin, origin, _conn), do: true
defp matching_origin?(allowed, _actual, _conn) when is_binary(allowed), do: false
defp matching_origin?(%Regex{} = allowed, actual, _conn), do: Regex.match?(allowed, actual)
defp matching_origin?({module, function, args}, actual, conn)
when is_atom(module) and is_atom(function) and is_list(args) do
apply(module, function, [conn, actual | args])
end
defp matching_origin?({module, function}, actual, _conn)
when is_atom(module) and is_atom(function) do
apply(module, function, [actual])
end
# Made public for testing.
@doc false
def allowed_preflight?(conn, %Options{} = opts) do
allowed_request_method?(conn, opts) and allowed_request_headers?(conn, opts)
end
defp allowed_request_method?(_conn, %Options{allow_methods: :all}) do
true
end
defp allowed_request_method?(conn, %Options{allow_methods: allow_methods} = opts) do
# We can safely assume there's an Access-Control-Request-Method header
# otherwise the request wouldn't have been identified as a preflight
# request.
[req_method | _] = get_req_header(conn, "access-control-request-method")
if req_method in @simple_methods or req_method in allow_methods do
true
else
execute_telemetry(conn, opts, [:rejected_request], %{
request_type: :preflight,
reason: {:req_method_not_allowed, req_method}
})
false
end
end
defp allowed_request_headers?(_conn, %Options{allow_headers: :all}) do
true
end
defp allowed_request_headers?(conn, %Options{allow_headers: allow_headers} = opts) do
non_allowed_headers =
for req_headers <- get_req_header(conn, "access-control-request-headers"),
req_headers = String.downcase(req_headers),
req_header <- Plug.Conn.Utils.list(req_headers),
not (req_header in @simple_headers or req_header in allow_headers),
do: req_header
if non_allowed_headers == [] do
true
else
execute_telemetry(conn, opts, [:rejected_request], %{
request_type: :preflight,
reason: {:req_headers_not_allowed, non_allowed_headers}
})
false
end
end
defp execute_telemetry(conn, %Options{} = opts, event_name, extra_meta) do
meta =
%{conn: conn}
|> Map.merge(extra_meta)
|> Map.merge(opts.telemetry_metadata)
:telemetry.execute([:corsica] ++ event_name, _measurements = %{}, meta)
end
end