Corsica

Plug-based swiss-army knife for CORS requests.

Corsica provides facilities for dealing with CORS requests and responses. It provides:

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:

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:

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:

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:

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.

Source

Summary

call(conn, opts)

Callback implementation of Plug.call/2

cors_req?(conn)

Checks whether a given connection holds a CORS request

init(opts)

Callback implementation of Plug.init/1

preflight_req?(conn)

Checks whether a given connection holds a preflight CORS request

put_cors_preflight_resp_headers(conn, opts)

Adds CORS response headers to a preflight request to conn

put_cors_simple_resp_headers(conn, opts)

Adds CORS response headers to a simple CORS request to conn

send_preflight_resp(conn, status \\ 200, body \\ "", opts)

Sends a CORS preflight response regardless of the request being a valid CORS request or not

Functions

call(conn, opts)

Callback implementation of Plug.call/2.

Source
cors_req?(conn)

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.

Source
init(opts)

Callback implementation of Plug.init/1.

Source
preflight_req?(conn)

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.

Source
put_cors_preflight_resp_headers(conn, opts)

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$/
]
Source
put_cors_simple_resp_headers(conn, opts)

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)
Source
send_preflight_resp(conn, status \\ 200, body \\ "", opts)

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
Source