Current section

Files

Jump to
erldns src admin erldns_admin.erl
Raw

src/admin/erldns_admin.erl

%% Copyright (c) 2012-2019, DNSimple Corporation
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% @doc Process for an administrative HTTP API.
%%
%% Provides zone quering and command-and-control functionality.
-module(erldns_admin).
-moduledoc """
Erldns admin API.
### Configuration:
This application will read from your `sys.config` the following example:
```erlang
{erldns, [
{admin, [
{credentials, {<<"username">>, <<"password">>}},
{port, 8083}
]}
]}
```
where `credentials` is a tuple of `username` and `password` as either strings or binaries,
and `port` is a valid Unix port to listen on.
""".
-define(DEFAULT_PORT, 8083).
-include_lib("kernel/include/logger.hrl").
-export([maybe_start/0, is_authorized/2]).
-doc """
Configuration parameters, see the module documentation for details.
""".
-type config() :: #{
port := 0..65535,
username := binary(),
password := binary()
}.
-doc "Common state for all handlers".
-opaque handler_state() :: #{
username := binary(),
password := binary()
}.
-export_type([config/0, handler_state/0]).
-type env() :: [{atom(), term()}].
-spec maybe_start() -> ok | {ok, pid()} | {error, any()}.
maybe_start() ->
case ensure_valid_config() of
disabled ->
ok;
false ->
error(bad_configuration);
Config ->
start(Config)
end.
-spec start(config()) -> {ok, pid()} | {error, any()}.
start(#{port := Port, username := Username, password := Password}) ->
State = #{username => Username, password => Password},
Dispatch = cowboy_router:compile(
[
{'_', [
{"/", erldns_admin_root_handler, State},
{"/zones/:zone_name", erldns_admin_zone_resource_handler, State},
{"/zones/:zone_name/:action", erldns_admin_zone_control_handler, State},
{"/zones/:zone_name/records[/:record_name]", erldns_admin_zone_records_resource_handler, State}
]}
]
),
TransportOpts = #{socket_opts => [inet, {ip, {0, 0, 0, 0}}, {port, Port}]},
ProtocolOpts = #{env => #{dispatch => Dispatch}},
cowboy:start_clear(?MODULE, TransportOpts, ProtocolOpts).
-doc false.
-spec is_authorized(cowboy_req:req(), handler_state()) ->
{true | {false, iodata()}, cowboy_req:req(), handler_state()}
| {stop, cowboy_req:req(), handler_state()}.
is_authorized(Req, #{username := ValidUsername, password := ValidPassword} = State) ->
maybe
{basic, GivenUsername, GivenPassword} ?= cowboy_req:parse_header(<<"authorization">>, Req),
true ?= is_binary_of_equal_size(GivenUsername),
true ?= is_binary_of_equal_size(GivenPassword),
true ?= crypto:hash_equals(GivenUsername, ValidUsername) andalso
crypto:hash_equals(GivenPassword, ValidPassword),
{true, Req, State}
else
_ ->
{{false, <<"Basic realm=\"erldns admin\"">>}, Req, State}
end.
-spec is_binary_of_equal_size(term()) -> boolean().
is_binary_of_equal_size(Bin) ->
is_binary(Bin) andalso byte_size(Bin) =:= byte_size(Bin).
-spec ensure_valid_config() -> false | disabled | config().
ensure_valid_config() ->
maybe
{true, Env} ?= env(),
{true, Port} ?= port(Env),
{true, Username, Password} ?= credentials(Env),
#{port => Port, username => Username, password => Password}
end.
-spec port(env()) -> {true, 1..65535} | false.
port(Env) ->
case proplists:get_value(port, Env, ?DEFAULT_PORT) of
Port when is_integer(Port), 0 < Port, Port =< 65535 ->
{true, Port};
OtherPort ->
?LOG_ERROR(#{what => erldns_admin_bad_config, port => OtherPort}),
false
end.
-spec credentials(env()) -> {true, binary(), binary()} | false.
credentials(Env) ->
case lists:keyfind(credentials, 1, Env) of
{credentials, {Username, Password}} when is_list(Username), is_list(Password) ->
{true, list_to_binary(Username), list_to_binary(Password)};
{credentials, {Username, Password}} when is_binary(Username), is_binary(Password) ->
{true, Username, Password};
OtherValue ->
?LOG_ERROR(#{what => erldns_admin_bad_config, credentials => OtherValue}),
false
end.
-spec env() -> {true, env()} | false | disabled.
env() ->
case application:get_env(erldns, admin) of
{ok, Env} when is_list(Env) -> {true, Env};
{ok, _} -> false;
_ -> disabled
end.