Current section

Files

Jump to
erldns src admin erldns_admin.erl
Raw

src/admin/erldns_admin.erl

-module(erldns_admin).
-moduledoc """
Erldns admin API.
### Configuration:
This application will read from your `sys.config` the following example:
```erlang
{erldns, [
{admin, [
{port, 8083},
{tls, false},
{credentials, {~"username", ~"password"}},
{middleware, [my_custom_middleware, another_middleware]},
{routes, [{~"/custom/:action", my_custom_route_handler, #{}}]}
]}
]}
```
The accepted values are:
- `port`: an integer between `1` and `65535` indicating the port to listen on
(default: `8083` without TLS and `8483` when TLS is enabled).
- `tls`: `false`, or `{true, SslOpts}` where `SslOpts` is a list of `t:ssl:tls_server_option/0`,
indicating whether to use TLS (default: `false`).
- `credentials`: `false` if no authentication is required, or a tuple of `{Username, Password}`
binary strings. If configured, all routes will require authentication (default: `false`).
- `middleware`: an optional list of cowboy compliant middleware modules to apply
to all admin API requests (default: `[]`).
- `routes`: an optional list of additional routes to add to the admin API.
This is a list of `cowboy_router:route_path()` elements that will be prepended
to the default routes (default: `[]`).
""".
-define(DEFAULT_CLEAR_PORT, 8083).
-define(DEFAULT_TLS_PORT, 8483).
-define(LOG_METADATA, #{domain => [erldns, admin]}).
-include_lib("kernel/include/logger.hrl").
-export([maybe_start/0]).
-ifdef(TEST).
-export_type([env/0]).
-endif.
-doc """
Configuration parameters, see the module documentation for details.
""".
-type config() :: #{
port := 0..65535,
tls := false | [ssl:tls_server_option()],
credentials := false | {binary(), binary()},
middleware => [module()],
routes => cowboy_router:routes()
}.
-type env() :: [{atom(), term()}].
-spec maybe_start() -> ok | {ok, pid()} | {error, term()}.
maybe_start() ->
case ensure_valid_config() of
disabled ->
ok;
error ->
error(bad_configuration);
Config ->
start(Config)
end.
-spec start(config()) -> {ok, pid()} | {error, term()}.
start(
#{
tls := Tls,
port := Port,
credentials := Credentials,
middleware := Middleware,
routes := AdditionalRoutes
}
) ->
Dispatch = cowboy_router:compile(default_routes(AdditionalRoutes)),
persistent_term:put(?MODULE, Dispatch),
ProtocolOpts = #{
env => #{dispatch => {persistent_term, ?MODULE}, credentials => Credentials},
middlewares => middlewares(Middleware, Credentials)
},
case Tls of
false ->
TransportOpts = socket_opts(Port),
cowboy:start_clear(?MODULE, #{socket_opts => TransportOpts}, ProtocolOpts);
SslOpts ->
TransportOpts = socket_opts(Port) ++ SslOpts,
cowboy:start_tls(?MODULE, #{socket_opts => TransportOpts}, ProtocolOpts)
end.
-spec middlewares([module()], false | dynamic()) -> [module()].
middlewares(Middleware, false) ->
[cowboy_router] ++ Middleware ++ [cowboy_handler];
middlewares(Middleware, _) ->
[erldns_admin_auth_middleware, cowboy_router] ++ Middleware ++ [cowboy_handler].
-spec default_routes([dynamic()]) -> cowboy_router:routes().
default_routes(AdditionalRoutes) ->
DefaultRoutes = [
{~"/", erldns_admin_root_handler, #{}},
{~"/zones/:zonename", erldns_admin_zone_handler, #{}},
{~"/zones/:zonename/records[/:record_name]", erldns_admin_zone_records_handler, #{}}
],
[{'_', AdditionalRoutes ++ DefaultRoutes}].
-spec ensure_valid_config() -> error | disabled | config().
ensure_valid_config() ->
maybe
{ok, Env} ?= env(),
{ok, Tls} ?= config_tls(Env),
{ok, Port} ?= config_port(Env, Tls),
{ok, Credentials} ?= config_credentials(Env),
{ok, Middleware} ?= config_middleware(Env),
{ok, Routes} ?= config_routes(Env),
#{
port => Port,
tls => Tls,
credentials => Credentials,
middleware => Middleware,
routes => Routes
}
end.
-spec socket_opts(inet:port_number()) -> [gen_tcp:option()].
socket_opts(Port) ->
[
inet6,
{ipv6_v6only, false},
{ip, any},
{port, Port},
{nodelay, true},
{keepalive, true},
{reuseport, true},
{reuseport_lb, true}
].
-spec config_port(env(), false | {true, dynamic()}) -> {ok, 1..65535} | error.
config_port(Env, MaybeTls) ->
DefaultPort =
case MaybeTls of
false -> ?DEFAULT_CLEAR_PORT;
_ -> ?DEFAULT_TLS_PORT
end,
case proplists:get_value(port, Env, DefaultPort) of
Port when is_integer(Port), 0 < Port, Port =< 65535 ->
{ok, Port};
OtherPort ->
?LOG_ERROR(
#{what => erldns_admin_bad_config, port => OtherPort},
?LOG_METADATA
),
error
end.
-spec config_tls(env()) -> {ok, false | [ssl:tls_server_option()]} | error.
config_tls(Env) ->
case proplists:get_value(tls, Env, false) of
false ->
{ok, false};
{true, Opts} when is_list(Opts) ->
{ok, Opts};
Other ->
?LOG_ERROR(
#{what => erldns_admin_bad_config, tls => Other},
?LOG_METADATA
),
error
end.
-spec config_credentials(env()) -> {ok, false | {binary(), binary()}} | error.
config_credentials(Env) ->
case lists:keyfind(credentials, 1, Env) of
false ->
{ok, false};
{credentials, {Username, Password}} when is_binary(Username), is_binary(Password) ->
{ok, {Username, Password}};
OtherValue ->
?LOG_ERROR(
#{what => erldns_admin_bad_config, credentials => OtherValue},
?LOG_METADATA
),
error
end.
-spec config_middleware(env()) -> {ok, [module()]} | error.
config_middleware(Env) ->
maybe
{middleware, Modules} ?= lists:keyfind(middleware, 1, Env),
[] ?= lists:filter(fun is_not_middleware/1, Modules),
{ok, Modules}
else
false ->
{ok, []};
OtherValue ->
?LOG_ERROR(
#{what => erldns_admin_bad_config, middleware => OtherValue},
?LOG_METADATA
),
error
end.
-spec is_not_middleware(module()) -> boolean().
is_not_middleware(Module) when is_atom(Module) ->
not (is_atom(Module) andalso {module, Module} =:= code:ensure_loaded(Module) andalso
erlang:function_exported(Module, execute, 2)).
-spec config_routes(env()) -> {ok, cowboy_router:routes()} | error.
config_routes(Env) ->
maybe
{routes, Routes} ?= lists:keyfind(routes, 1, Env),
true ?= is_list(Routes),
{ok, Routes}
else
false ->
{ok, []};
OtherValue ->
?LOG_ERROR(
#{what => erldns_admin_bad_config, routes => OtherValue},
?LOG_METADATA
),
error
end.
-spec env() -> {ok, env()} | error | disabled.
env() ->
case application:get_env(erldns, admin) of
{ok, Env} when is_list(Env) -> {ok, Env};
{ok, _} -> error;
_ -> disabled
end.