Packages

A CORS Builder, performing validation and injection of CORS for misp, wisp and any framework!

Current section

Files

Jump to
cors_builder src cors_builder.erl
Raw

src/cors_builder.erl

-module(cors_builder).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]).
-define(FILEPATH, "src/cors_builder.gleam").
-export([new/0, allow_all_origins/1, allow_origin/2, expose_header/2, max_age/2, allow_credentials/1, allow_method/2, allow_header/2, set_cors/2, set_cors_multiple_origin/3, mist_middleware/3, wisp_middleware/3]).
-export_type([origin/0, cors/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" `cors_builder` provides an easy way to build and inject your CORS configuration\n"
" in your server. The package tries to remains as simplest as possible, while\n"
" guaranteeing type-safety and correctness of the CORS configuration.\n"
"\n"
" ## Quickstart\n"
"\n"
" Import the `cors_builder` package, and configure your CORS. Finally, use the\n"
" correct corresponding middleware for your server, and you're done!\n"
"\n"
" ```\n"
" import cors_builder as cors\n"
" import gleam/http\n"
" import mist\n"
" import wisp.{type Request, type Response}\n"
"\n"
" // Dummy example.\n"
" fn cors() {\n"
" cors.new()\n"
" |> cors.allow_origin(\"http://localhost:3000\")\n"
" |> cors.allow_origin(\"http://localhost:4000\")\n"
" |> cors.allow_method(http.Get)\n"
" |> cors.allow_method(http.Post)\n"
" }\n"
"\n"
" fn handler(req: Request) -> Response {\n"
" use req <- cors.wisp_middleware(req, cors())\n"
" wisp.ok()\n"
" }\n"
"\n"
" fn main() {\n"
" handler\n"
" |> wisp.mist_handler(secret_key)\n"
" |> mist.new()\n"
" |> mist.port(3000)\n"
" |> mist.start_http()\n"
" }\n"
" ```\n"
"\n"
" ## Low-level functions\n"
"\n"
" If you're building your framework or you know what you're doing, you should\n"
" take a look at [`set_cors`](#set_cors) and\n"
" [`set_cors_multiple_origin`](#set_cors_multiple_origin). They allow to\n"
" inject the CORS in your response, and it allows you to create your\n"
" middleware to use with the bare CORS data.\n"
"\n"
" If you're not building your framework, you should _probably_ heads to [`wisp`](https://hexdocs.pm/wisp)\n"
" to get you started. It's better to familiarize with the ecosystem before\n"
" jumping right in your custom code.\n"
).
-opaque origin() :: wildcard | {origin, gleam@set:set(binary())}.
-opaque cors() :: {cors,
gleam@option:option(origin()),
gleam@set:set(binary()),
gleam@option:option(integer()),
gleam@option:option(boolean()),
gleam@set:set(gleam@http:method()),
gleam@set:set(binary())}.
-file("src/cors_builder.gleam", 93).
?DOC(
" Creates an empty CORS object. It will not contains anything by default.\n"
" If you're using it directly, no headers will be added to the response.\n"
).
-spec new() -> cors().
new() ->
{cors, none, gleam@set:new(), none, none, gleam@set:new(), gleam@set:new()}.
-file("src/cors_builder.gleam", 110).
?DOC(
" Allow all domains to access your server.\n"
"\n"
" Be extremely careful, you should not use this function in production!\n"
" Allowing all origins can easily be a huge security flaw!\n"
" Allow only the origins you need, and use this function only locally,\n"
" in dev mode.\n"
).
-spec allow_all_origins(cors()) -> cors().
allow_all_origins(Cors) ->
Allow_origin = {some, wildcard},
_record = Cors,
{cors,
Allow_origin,
erlang:element(3, _record),
erlang:element(4, _record),
erlang:element(5, _record),
erlang:element(6, _record),
erlang:element(7, _record)}.
-file("src/cors_builder.gleam", 115).
-spec invalid_uri(binary()) -> boolean().
invalid_uri(Origin) ->
_pipe = gleam_stdlib:uri_parse(Origin),
_pipe@1 = gleam@result:is_error(_pipe),
gleam@function:tap(
_pipe@1,
fun(Value) ->
gleam@bool:guard(
not Value,
nil,
fun() ->
gleam_stdlib:println(
<<<<"Your provided origin: \""/utf8, Origin/binary>>/binary,
"\" is not a valid URI."/utf8>>
)
end
)
end
).
-file("src/cors_builder.gleam", 135).
?DOC(
" Allow a specific domain to access your server. The domain must be a valid\n"
" URI, conformant to RFC 3986. In case it's not conformant, a warning will be\n"
" emitted, and Cors won't be changed.\n"
" You can specify multiple domains to access your server. In this case, call\n"
" the function multiple times on `Cors` data.\n"
" ```\n"
" fn cors() {\n"
" cors.new()\n"
" |> cors.allow_origin(\"domain\")\n"
" |> cors.allow_origin(\"domain2\")\n"
" }\n"
).
-spec allow_origin(cors(), binary()) -> cors().
allow_origin(Cors, Origin) ->
gleam@bool:guard(
invalid_uri(Origin),
Cors,
fun() ->
Allow_origin = case erlang:element(2, Cors) of
{some, wildcard} ->
{some, wildcard};
{some, {origin, Content}} ->
{some, {origin, gleam@set:insert(Content, Origin)}};
none ->
{some, {origin, gleam@set:from_list([Origin])}}
end,
_record = Cors,
{cors,
Allow_origin,
erlang:element(3, _record),
erlang:element(4, _record),
erlang:element(5, _record),
erlang:element(6, _record),
erlang:element(7, _record)}
end
).
-file("src/cors_builder.gleam", 155).
?DOC(
" Expose headers in the resulting request.\n"
" You can specify multiple headers to access your server. In this case, call\n"
" the function multiple times on `Cors` data.\n"
" ```\n"
" fn cors() {\n"
" cors.new()\n"
" |> cors.expose_header(\"content-type\")\n"
" |> cors.expose_header(\"vary\")\n"
" }\n"
" ```\n"
).
-spec expose_header(cors(), binary()) -> cors().
expose_header(Cors, Header) ->
Expose_headers = gleam@set:insert(erlang:element(3, Cors), Header),
_record = Cors,
{cors,
erlang:element(2, _record),
Expose_headers,
erlang:element(4, _record),
erlang:element(5, _record),
erlang:element(6, _record),
erlang:element(7, _record)}.
-file("src/cors_builder.gleam", 164).
?DOC(
" Set an amount of seconds during which CORS requests can be cached.\n"
" When using `max_age`, the browser will issue one request `OPTIONS` at first,\n"
" and will reuse the result of that request for the specified amount of time.\n"
" Once the cache expired, a new `OPTIONS` request will be made.\n"
).
-spec max_age(cors(), integer()) -> cors().
max_age(Cors, Age) ->
Max_age = {some, Age},
_record = Cors,
{cors,
erlang:element(2, _record),
erlang:element(3, _record),
Max_age,
erlang:element(5, _record),
erlang:element(6, _record),
erlang:element(7, _record)}.
-file("src/cors_builder.gleam", 182).
?DOC(
" Allow credentials to be sent in the request. Credentials take form of\n"
" username and password, stored in cookies most of the time.\n"
"\n"
" Be extremely careful with this header, and consider it with caution, mainly\n"
" for legacy systems relying on cookies or for systems aware of the danger of\n"
" cookies, because of [CSRF attacks](https://developer.mozilla.org/en-US/docs/Glossary/CSRF).\n"
" You probably don't really need it if you use lustre or any modern framework\n"
" you'll find in the gleam ecosystem!\n"
"\n"
" When you can, prefer using some modern system, like OAuth2 or rely on a\n"
" framework doing the authentication for you. A simple and secured way to\n"
" authenticate your users is to use the `authorization` header, with a `Bearer`\n"
" token.\n"
).
-spec allow_credentials(cors()) -> cors().
allow_credentials(Cors) ->
Allow_credentials = {some, true},
_record = Cors,
{cors,
erlang:element(2, _record),
erlang:element(3, _record),
erlang:element(4, _record),
Allow_credentials,
erlang:element(6, _record),
erlang:element(7, _record)}.
-file("src/cors_builder.gleam", 199).
?DOC(
" Allow methods to be used in subsequent CORS requests.\n"
" You can specify multiple allowed methods. In this case, call the function\n"
" multiple times on `Cors` data.\n"
" ```\n"
" import gleam/http\n"
"\n"
" fn cors() {\n"
" cors.new()\n"
" |> cors.allow_method(http.Get)\n"
" |> cors.allow_method(http.Post)\n"
" }\n"
" ```\n"
).
-spec allow_method(cors(), gleam@http:method()) -> cors().
allow_method(Cors, Method) ->
Allow_methods = gleam@set:insert(erlang:element(6, Cors), Method),
_record = Cors,
{cors,
erlang:element(2, _record),
erlang:element(3, _record),
erlang:element(4, _record),
erlang:element(5, _record),
Allow_methods,
erlang:element(7, _record)}.
-file("src/cors_builder.gleam", 214).
?DOC(
" All header to be sent to the server.\n"
" You can specify multiple headers to send to your server. In this case, call\n"
" the function multiple times on `Cors` data.\n"
" ```\n"
" fn cors() {\n"
" cors.new()\n"
" |> cors.allow_header(\"content-type\")\n"
" |> cors.allow_header(\"origin\")\n"
" }\n"
" ```\n"
).
-spec allow_header(cors(), binary()) -> cors().
allow_header(Cors, Header) ->
Allow_headers = gleam@set:insert(erlang:element(7, Cors), Header),
_record = Cors,
{cors,
erlang:element(2, _record),
erlang:element(3, _record),
erlang:element(4, _record),
erlang:element(5, _record),
erlang:element(6, _record),
Allow_headers}.
-file("src/cors_builder.gleam", 222).
-spec warn_if_origin_empty(binary()) -> nil.
warn_if_origin_empty(Origin) ->
case Origin of
<<""/utf8>> ->
gleam_stdlib:println(
<<"origin is empty, but you have multiple allowed domains in your CORS configuration. Are you sure you're calling set_cors_multiple_origin and not set_cors?"/utf8>>
);
_ ->
nil
end.
-file("src/cors_builder.gleam", 232).
-spec set_allowed_origin(cors(), binary()) -> fun((gleam@http@response:response(RKS)) -> gleam@http@response:response(RKS)).
set_allowed_origin(Cors, Origin) ->
Hd = <<"access-control-allow-origin"/utf8>>,
case erlang:element(2, Cors) of
none ->
fun gleam@function:identity/1;
{some, wildcard} ->
fun(_capture) ->
gleam@http@response:set_header(_capture, Hd, <<"*"/utf8>>)
end;
{some, {origin, Origins}} ->
Origins@1 = gleam@set:to_list(Origins),
case Origins@1 of
[O] ->
fun(_capture@1) ->
gleam@http@response:set_header(_capture@1, Hd, O)
end;
_ ->
warn_if_origin_empty(Origin),
Not_origin = not gleam@list:contains(Origins@1, Origin),
gleam@bool:guard(
Not_origin,
fun gleam@function:identity/1,
fun() -> fun(Res) -> _pipe = Res,
_pipe@1 = gleam@http@response:set_header(
_pipe,
Hd,
Origin
),
gleam@http@response:set_header(
_pipe@1,
<<"vary"/utf8>>,
<<"origin"/utf8>>
) end end
)
end
end.
-file("src/cors_builder.gleam", 256).
-spec set_expose_headers(gleam@http@response:response(RHE), cors()) -> gleam@http@response:response(RHE).
set_expose_headers(Res, Cors) ->
Hd = <<"access-control-expose-headers"/utf8>>,
Ls = gleam@set:to_list(erlang:element(3, Cors)),
gleam@bool:guard(gleam@list:is_empty(Ls), Res, fun() -> _pipe = Ls,
_pipe@1 = gleam@string:join(_pipe, <<","/utf8>>),
gleam@http@response:set_header(Res, Hd, _pipe@1) end).
-file("src/cors_builder.gleam", 265).
-spec set_max_age(gleam@http@response:response(RHH), cors()) -> gleam@http@response:response(RHH).
set_max_age(Res, Cors) ->
Hd = <<"access-control-max-age"/utf8>>,
_pipe = erlang:element(4, Cors),
_pipe@1 = gleam@option:map(
_pipe,
fun(A) ->
gleam@http@response:set_header(Res, Hd, erlang:integer_to_binary(A))
end
),
gleam@option:unwrap(_pipe@1, Res).
-file("src/cors_builder.gleam", 272).
-spec set_allow_credentials(gleam@http@response:response(RHK), cors()) -> gleam@http@response:response(RHK).
set_allow_credentials(Res, Cors) ->
Hd = <<"access-control-allow-credentials"/utf8>>,
_pipe = erlang:element(5, Cors),
_pipe@1 = gleam@option:map(
_pipe,
fun(_) -> gleam@http@response:set_header(Res, Hd, <<"true"/utf8>>) end
),
gleam@option:unwrap(_pipe@1, Res).
-file("src/cors_builder.gleam", 279).
-spec method_to_string(gleam@http:method()) -> binary().
method_to_string(Method) ->
case Method of
get ->
<<"GET"/utf8>>;
post ->
<<"POST"/utf8>>;
head ->
<<"HEAD"/utf8>>;
put ->
<<"PUT"/utf8>>;
delete ->
<<"DELETE"/utf8>>;
trace ->
<<"TRACE"/utf8>>;
connect ->
<<"CONNECT"/utf8>>;
options ->
<<"OPTIONS"/utf8>>;
patch ->
<<"PATCH"/utf8>>;
{other, Content} ->
Content
end.
-file("src/cors_builder.gleam", 294).
-spec set_allow_methods(gleam@http@response:response(RHO), cors()) -> gleam@http@response:response(RHO).
set_allow_methods(Res, Cors) ->
Hd = <<"access-control-allow-methods"/utf8>>,
Methods = gleam@set:to_list(erlang:element(6, Cors)),
gleam@bool:guard(
gleam@list:is_empty(Methods),
Res,
fun() -> _pipe = Methods,
_pipe@1 = gleam@list:map(_pipe, fun method_to_string/1),
_pipe@2 = gleam@string:join(_pipe@1, <<","/utf8>>),
gleam@http@response:set_header(Res, Hd, _pipe@2) end
).
-file("src/cors_builder.gleam", 304).
-spec set_allow_headers(gleam@http@response:response(RHR), cors()) -> gleam@http@response:response(RHR).
set_allow_headers(Res, Cors) ->
Hd = <<"access-control-allow-headers"/utf8>>,
Headers = gleam@set:to_list(erlang:element(7, Cors)),
case gleam@list:is_empty(Headers) of
true ->
Res;
false ->
_pipe = Headers,
_pipe@1 = gleam@string:join(_pipe, <<","/utf8>>),
gleam@http@response:set_header(Res, Hd, _pipe@1)
end.
-file("src/cors_builder.gleam", 316).
-spec set_response(
gleam@http@response:response(RHU),
cors(),
gleam@option:option(binary())
) -> gleam@http@response:response(RHU).
set_response(Res, Cors, Origin) ->
_pipe = Res,
_pipe@1 = (set_allowed_origin(
Cors,
gleam@option:unwrap(Origin, <<""/utf8>>)
))(_pipe),
_pipe@2 = set_expose_headers(_pipe@1, Cors),
_pipe@3 = set_max_age(_pipe@2, Cors),
_pipe@4 = set_allow_credentials(_pipe@3, Cors),
_pipe@5 = set_allow_methods(_pipe@4, Cors),
set_allow_headers(_pipe@5, Cors).
-file("src/cors_builder.gleam", 333).
?DOC(
" Set CORS headers on a response. Should be used in your handler.\n"
" In case you're using a framework, it probably already implements it.\n"
" If you're using mist or wisp, use the corresponding provided middlewares,\n"
" ([mist_middleware](#mist_middleware)) and ([wisp_middleware](#wisp_middleware)) and do not\n"
" use this \"low-level\" function.\n"
).
-spec set_cors(gleam@http@response:response(RHY), cors()) -> gleam@http@response:response(RHY).
set_cors(Res, Cors) ->
set_response(Res, Cors, none).
-file("src/cors_builder.gleam", 343).
?DOC(
" Set CORS headers on a response. Should be used when you have multiple\n"
" allowed domains. Should be used in your handler.\n"
" In case you're using a framework, it probably already implements it.\n"
" If you're using mist or wisp, use the corresponding provided middlewares,\n"
" ([mist_middleware](#mist_middleware)) and ([wisp_middleware](#wisp_middleware)) and do not\n"
" use this \"low-level\" function.\n"
).
-spec set_cors_multiple_origin(
gleam@http@response:response(RIB),
cors(),
binary()
) -> gleam@http@response:response(RIB).
set_cors_multiple_origin(Res, Cors, Origin) ->
set_response(Res, Cors, {some, Origin}).
-file("src/cors_builder.gleam", 351).
-spec find_origin(gleam@http@request:request(any())) -> gleam@option:option(binary()).
find_origin(Req) ->
_pipe = erlang:element(3, Req),
_pipe@1 = gleam@list:find(
_pipe,
fun(H) -> gleam@pair:first(H) =:= <<"origin"/utf8>> end
),
_pipe@2 = gleam@result:map(_pipe@1, fun gleam@pair:second/1),
gleam@option:from_result(_pipe@2).
-file("src/cors_builder.gleam", 358).
-spec middleware(
RIH,
gleam@http@request:request(RII),
cors(),
fun((gleam@http@request:request(RII)) -> gleam@http@response:response(RIH))
) -> gleam@http@response:response(RIH).
middleware(Empty, Req, Cors, Handler) ->
Res = case erlang:element(2, Req) of
options ->
gleam@http@response:set_body(gleam@http@response:new(204), Empty);
_ ->
Handler(Req)
end,
_pipe = Req,
_pipe@1 = find_origin(_pipe),
_pipe@2 = gleam@option:map(
_pipe@1,
fun(_capture) -> set_cors_multiple_origin(Res, Cors, _capture) end
),
gleam@option:unwrap(_pipe@2, Res).
-file("src/cors_builder.gleam", 376).
?DOC(
" Intercepts the request for mist and handles CORS directly without worrying\n"
" about it. Provide your CORS configuration, and you're good to go!\n"
).
-spec mist_middleware(
gleam@http@request:request(mist@internal@http:connection()),
cors(),
fun((gleam@http@request:request(mist@internal@http:connection())) -> gleam@http@response:response(mist:response_data()))
) -> gleam@http@response:response(mist:response_data()).
mist_middleware(Req, Cors, Handler) ->
_pipe = gleam@bytes_tree:new(),
_pipe@1 = {bytes, _pipe},
middleware(_pipe@1, Req, Cors, Handler).
-file("src/cors_builder.gleam", 388).
?DOC(
" Intercepts the request for wisp and handles CORS directly without worrying\n"
" about it. Provide your CORS configuration and you're good to go!\n"
).
-spec wisp_middleware(
gleam@http@request:request(wisp@internal:connection()),
cors(),
fun((gleam@http@request:request(wisp@internal:connection())) -> gleam@http@response:response(wisp:body()))
) -> gleam@http@response:response(wisp:body()).
wisp_middleware(Req, Cors, Handler) ->
middleware(empty, Req, Cors, Handler).