Corsica
Plug-based swiss-army knife for CORS requests.
Corsica provides facilities for dealing with CORS requests and responses. It provides:
- low-level functions that lets 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 for dealing with CORS requests.
How it works
Corsica is compliant with the W3C CORS specification. 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 below.
When some options that are not mandatory and have no default value (such
:max_age
) are not present, the relative header will often not be sent at
all. This is compliant with the specification and at the same time it reduces
(even if by a handful of bytes) the size of the response.
Follows a list of all the supported response headers:
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials
Access-Control-Expose-Headers
Access-Control-Max-Age
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: 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) will often result in 404 errors since no route responds
an OPTIONS
request.
defmodule MyApp.Endpoint do
plug Head
plug Corsica, max_age: 600, origins: "*", expose_headers: ~w(X-Foo)
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.
defmodule MyApp.CORS do
use Corsica.Router
@opts [
max_age: 600,
allow_credentials: true,
allow_headers: ~w(X-Secret-Token),
origins: "*",
]
resource "/public/*", @opts
resource "/*", Keyword.merge(@opts, origins: "http://foo.com")
end
defmodule MyApp.Endpoint do
plug Logger
plug MyApp.CORS
plug MyApp.Router
end
Origins
Allowed origins can be specified by passing the :origins
options either when
Corsica
is used or when the Corsica
plug is plugged to a pipeline.
:origins
can be a single value or a list of values. "*"
can only appear as
a single value. The default value is "*"
.
Origins can be specified either as:
- strings - the actual origin and the allowed origin have to be identical
- regexes - the actual origin has to match the allowed regex
- functions with a type
(binary -> boolean)
- the function applied to the actual origin has to returntrue
The value of the access-control-allow-origin header
The :origins
option directly influences the value of the
access-control-allow-origin
header. When :origins
is "*"
, the
access-control-allow-origin
header is set to *
as well. If the request’s
origin is allowed an :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).
Vary header
If :origins
is a list with more than one value and the request origin
matches, then a Vary: Origin
header is added to the response.
Options
Besides :origins
, the options that can be passed to the use
macro, to
Corsica.DSL.resource/2
and to the Corsica
plug (along with their default
values) are:
:allow_headers
- is a list of headers (as binaries). Sets the value of theaccess-control-allow-headers
header used with preflight requests. Defaults to[]
(no headers are allowed).:allow_methods
- is a list of HTTP methods (as binaries). Sets the value of theaccess-control-allow-methods
header used with preflight requests. Defaults to["HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"]
.:allow_credentials
- is a boolean. Iftrue
, sends theaccess-control-allow-credentials
with valuetrue
. Iffalse
, prevents that header from being sent at all. If:origins
is set to"*"
and:allow_credentials
is set totrue
, then the value of theaccess-control-allow-origin
header will always be the value of theorigin
request header (as per the W3C CORS specification) and not*
. Defaults tofalse
.:expose_headers
- is a list of headers (as binaries). Sets the value of theaccess-control-expose-headers
response header. This option does not have a default value; if it’s not provided, theaccess-control-expose-headers
header is not sent at all.:max_age
- is an integer or a binary. Sets the value of theaccess-control-max-age
header used with preflight requests. This option does not have a default value; if it’s not provided, theaccess-control-max-age
header is not sent at all.
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
andAccess-Control-Request-Headers
headers)
Logging
Corsica supports basic logging functionalities; it can log whether a CORS
request is a valid one, what CORS headers are added to a response and similar
information. By default logging is disabled. This can be changed by changing
the value of the :log_level
option for the :corsica
application. For
example, in config/config.exs
:
config :corsica, log_level: :info
The value of the :log_level
option is used, as the name implies, as the
logging level. With the example above, Corsica will log everything at the
info
level.
Summary
Callback implementation for c:Plug.call/2
Checks whether a given connection holds a CORS request
Callback implementation for c:Plug.init/1
Checks whether a given connection holds a preflight CORS request
Adds CORS response headers to a preflight request to conn
Adds CORS response headers to a simple CORS request to conn
Sends a CORS preflight response regardless of the request being a valid CORS request or not
Functions
Specs
cors_req?(Plug.Conn.t) :: boolean
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.
Specs
preflight_req?(Plug.Conn.t) :: boolean
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
.
Specs
put_cors_preflight_resp_headers(Plug.Conn.t, Keyword.t) :: Plug.Conn.t
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.
If the request is a valid one, the following headers will always 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
Access-Control-Max-Age
Examples
put_cors_preflight_resp_headers conn, [
max_age: 86400,
allow_headers: ~w(X-Header),
origins: ~r/w+.foo.com$/
]
Specs
put_cors_simple_resp_headers(Plug.Conn.t, Keyword.t) :: Plug.Conn.t
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.
If the CORS request is valid, the following response headers are always set:
Access-Control-Allow-Origin
and the following headers are optionally set (if the corresponding option is present):
Access-Control-Expose-Headers
Access-Control-Allow-Credentials
Examples
put_cors_simple_resp_headers(conn, origins: "*", allow_credentials: true)
Specs
send_preflight_resp(Plug.Conn.t, 100 .. 599, binary, Keyword.t) :: Plug.Conn.t
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
.
Examples
defmodule MyRouter do
use Plug.Router
plug :match
plug :dispatch
options "/foo", do: Corsica.send_preflight_resp(conn, @cors_opts)
get "/foo", do: send_resp(conn, 200, "ok")
end