163 lines
5.4 KiB
Elixir
163 lines
5.4 KiB
Elixir
defmodule Corsica.Router do
|
|
@moduledoc """
|
|
A router to handle and respond to CORS requests.
|
|
|
|
This module provides facilities for creating
|
|
[`Plug.Router`](https://hexdocs.pm/plug/Plug.Router.html)-based routers that
|
|
handle CORS requests. A generated router will handle a CORS request by:
|
|
|
|
* responding to it if it's a preflight request (refer to
|
|
`Corsica.send_preflight_resp/4` for more information) or
|
|
* adding the right CORS headers to the `Plug.Conn` connection if it's a
|
|
valid CORS request.
|
|
|
|
When a module calls `use Corsica.Router`, it can pass the same options that
|
|
can be passed to the `Corsica` plug to the `use` call. Look at the
|
|
documentation for the `Corsica` module for more information about these
|
|
options.
|
|
|
|
CORS rules in a `Corsica.Router` can be defined through the `resource/2`
|
|
macro.
|
|
|
|
> #### `use Corsica.Router` {: .info}
|
|
>
|
|
> When you `use Corsica.Router`, your module will become a `Plug.Router`.
|
|
|
|
## Examples
|
|
|
|
defmodule MyApp.CORS do
|
|
use Corsica.Router,
|
|
origins: ["http://foo.com", "http://bar.com"],
|
|
allow_credentials: true,
|
|
max_age: 600
|
|
|
|
resource "/*"
|
|
|
|
# We can override single settings as well.
|
|
resource "/public/*", allow_credentials: false
|
|
end
|
|
|
|
Now in your application's endpoint:
|
|
|
|
defmodule MyApp.Endpoint do
|
|
plug Plug.Head
|
|
plug MyApp.CORS
|
|
plug Plug.Static
|
|
plug MyApp.Router
|
|
end
|
|
|
|
Note that a `Corsica.Router` router will always define a match-all route after
|
|
the `resource` routes; this match-all route will simply return the connection
|
|
unchanged, effectively continuing with the plug pipeline.
|
|
"""
|
|
|
|
# A Corsica.Router is a wrapper around Plug.Router: the resource/2 macro will
|
|
# just register resources (as a {route, options} tuple) in the @corsica_routes
|
|
# module attribute, then Corsica.Router.__before_compile__/1 will take care of
|
|
# converting the routes in @corsica_routes to calls to Plug.Router.options/2
|
|
# and Plug.Router.match/2. __before_compile__/1 will also take care of
|
|
# defining the final match-all clause for this router.
|
|
@doc false
|
|
defmacro __using__(opts) do
|
|
quote do
|
|
import unquote(__MODULE__)
|
|
|
|
@corsica_router_opts unquote(opts)
|
|
|
|
Module.register_attribute(__MODULE__, :corsica_routes, accumulate: true)
|
|
@before_compile Corsica.Router
|
|
|
|
use Plug.Router
|
|
plug :match
|
|
plug :dispatch
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Defines a CORS-enabled resource.
|
|
|
|
This macro takes advantage of the macros defined by
|
|
[`Plug.Router`](https://hexdocs.pm/plug/Plug.Router.html) (like `options/3`
|
|
and `match/3`) in order to define regular `Plug.Router`-like routes that
|
|
efficiently match on the request url; the bodies of the autogenerated routes
|
|
just perform a couple of checks before calling either
|
|
`Corsica.put_cors_simple_resp_headers/2` or `Corsica.send_preflight_resp/4`.
|
|
|
|
Note that if the request is a CORS preflight request (whether it's a valid one
|
|
or not), a response is immediately sent to the client (whether the request is
|
|
a valid one or not). This behaviour, combined with the definition of an
|
|
additional `OPTIONS` route to `route`, makes `Corsica.Router` ideal to just
|
|
put before any router in a plug pipeline, letting it handle preflight requests
|
|
by itself.
|
|
|
|
The options given to `resource/2` are merged with the default options like it
|
|
happens with the rest of the functions in the `Corsica` module. The `resource/2`
|
|
macro also accepts the following options (similar to `Plug.Router.match/3`):
|
|
|
|
* `:host` - the host which the route should match. Defaults to `nil`, meaning
|
|
no host match, but can be a string like `"example.com"` or a string ending with `.`,
|
|
like `"subdomain."`, for a subdomain match.
|
|
|
|
## Examples
|
|
|
|
resource "/foo", origins: "*"
|
|
resource "/wildcards/are/ok/*", max_age: 600
|
|
resource "/only/on/subdomain", host: "mysubdomain."
|
|
|
|
"""
|
|
defmacro resource(route, opts \\ []) do
|
|
quote do
|
|
@corsica_routes {unquote(route), unquote(Macro.escape(opts))}
|
|
end
|
|
end
|
|
|
|
defmacro __before_compile__(env) do
|
|
global_opts = Module.get_attribute(env.module, :corsica_router_opts)
|
|
routes = Module.get_attribute(env.module, :corsica_routes) |> Enum.reverse()
|
|
|
|
quote bind_quoted: [global_opts: Macro.escape(global_opts), routes: routes] do
|
|
for {route, opts} <- routes do
|
|
{match_opts, opts} = Keyword.split(opts, [:host])
|
|
|
|
corsica_opts =
|
|
global_opts
|
|
|> Keyword.merge(opts)
|
|
|> Corsica.sanitize_opts()
|
|
|> Macro.escape()
|
|
|
|
# Plug.Router wants this.
|
|
route = route <> if(String.ends_with?(route, "*"), do: "_", else: "")
|
|
|
|
options route, match_opts do
|
|
conn = var!(conn)
|
|
|
|
if Corsica.preflight_req?(conn) do
|
|
Corsica.send_preflight_resp(conn, unquote(corsica_opts))
|
|
else
|
|
conn
|
|
end
|
|
end
|
|
|
|
match route, match_opts do
|
|
conn = var!(conn)
|
|
|
|
if Corsica.cors_req?(conn) do
|
|
Corsica.put_cors_simple_resp_headers(conn, unquote(corsica_opts))
|
|
else
|
|
conn
|
|
end
|
|
end
|
|
end
|
|
|
|
# If there is a match-all route like "/*", we don't do anything,
|
|
# otherwise we add a match-all clause that just returns the
|
|
# connection unchanged.
|
|
unless Enum.any?(routes, &match?({"/*", _opts}, &1)) do
|
|
match _ do
|
|
var!(conn)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|