2025-04-16 10:03:13 -03:00

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