280 lines
8.8 KiB
Erlang
280 lines
8.8 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% Copyright (c) 2020-2023 EMQ Technologies Co., Ltd. All Rights Reserved.
|
|
%%
|
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
|
%% you may not use this file except in compliance with the License.
|
|
%% You may obtain a copy of the License at
|
|
%%
|
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
|
%%
|
|
%% Unless required by applicable law or agreed to in writing, software
|
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
%% See the License for the specific language governing permissions and
|
|
%% limitations under the License.
|
|
%%--------------------------------------------------------------------
|
|
|
|
-module(emqx_dashboard).
|
|
|
|
-export([
|
|
start_listeners/0,
|
|
start_listeners/1,
|
|
stop_listeners/1,
|
|
stop_listeners/0,
|
|
list_listeners/0,
|
|
wait_for_listeners/0
|
|
]).
|
|
|
|
%% Authorization
|
|
-export([authorize/1]).
|
|
|
|
-include_lib("emqx/include/logger.hrl").
|
|
-include_lib("emqx/include/http_api.hrl").
|
|
-include_lib("emqx/include/emqx_release.hrl").
|
|
|
|
-define(EMQX_MIDDLE, emqx_dashboard_middleware).
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Start/Stop Listeners
|
|
%%--------------------------------------------------------------------
|
|
|
|
start_listeners() ->
|
|
start_listeners(listeners()).
|
|
|
|
stop_listeners() ->
|
|
stop_listeners(listeners()).
|
|
|
|
start_listeners(Listeners) ->
|
|
{ok, _} = application:ensure_all_started(minirest),
|
|
Authorization = {?MODULE, authorize},
|
|
GlobalSpec = #{
|
|
openapi => "3.0.0",
|
|
info => #{title => emqx_api_name(), version => emqx_release_version()},
|
|
servers => [#{url => emqx_dashboard_swagger:base_path()}],
|
|
components => #{
|
|
schemas => #{},
|
|
'securitySchemes' => #{
|
|
'basicAuth' => #{
|
|
type => http,
|
|
scheme => basic,
|
|
description =>
|
|
<<"Authorize with [API Keys](https://www.emqx.io/docs/en/v5.0/admin/api.html#api-keys)">>
|
|
},
|
|
'bearerAuth' => #{
|
|
type => http,
|
|
scheme => bearer,
|
|
description => <<"Authorize with Bearer Token">>
|
|
}
|
|
}
|
|
}
|
|
},
|
|
BaseMinirest = #{
|
|
base_path => emqx_dashboard_swagger:base_path(),
|
|
modules => minirest_api:find_api_modules(apps()),
|
|
authorization => Authorization,
|
|
security => [#{'basicAuth' => []}, #{'bearerAuth' => []}],
|
|
swagger_global_spec => GlobalSpec,
|
|
dispatch => dispatch(),
|
|
middlewares => [?EMQX_MIDDLE, cowboy_router, cowboy_handler]
|
|
},
|
|
{OkListeners, ErrListeners} =
|
|
lists:foldl(
|
|
fun({Name, Protocol, Bind, RanchOptions, ProtoOpts}, {OkAcc, ErrAcc}) ->
|
|
Minirest = BaseMinirest#{protocol => Protocol, protocol_options => ProtoOpts},
|
|
case minirest:start(Name, RanchOptions, Minirest) of
|
|
{ok, _} ->
|
|
?ULOG("Listener ~ts on ~ts started.~n", [
|
|
Name, emqx_listeners:format_bind(Bind)
|
|
]),
|
|
{[Name | OkAcc], ErrAcc};
|
|
{error, _Reason} ->
|
|
%% Don't record the reason because minirest already does(too much logs noise).
|
|
{OkAcc, [Name | ErrAcc]}
|
|
end
|
|
end,
|
|
{[], []},
|
|
listeners(ensure_ssl_cert(Listeners))
|
|
),
|
|
case ErrListeners of
|
|
[] ->
|
|
optvar:set(emqx_dashboard_listeners_ready, OkListeners),
|
|
ok;
|
|
_ ->
|
|
{error, ErrListeners}
|
|
end.
|
|
|
|
stop_listeners(Listeners) ->
|
|
optvar:unset(emqx_dashboard_listeners_ready),
|
|
[
|
|
begin
|
|
case minirest:stop(Name) of
|
|
ok ->
|
|
?ULOG("Stop listener ~ts on ~ts successfully.~n", [
|
|
Name, emqx_listeners:format_bind(Bind)
|
|
]);
|
|
{error, not_found} ->
|
|
?SLOG(warning, #{msg => "stop_listener_failed", name => Name, bind => Bind})
|
|
end
|
|
end
|
|
|| {Name, _, Bind, _, _} <- listeners(Listeners)
|
|
],
|
|
ok.
|
|
|
|
wait_for_listeners() ->
|
|
optvar:read(emqx_dashboard_listeners_ready).
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% internal
|
|
|
|
apps() ->
|
|
[
|
|
App
|
|
|| {App, _, _} <- application:loaded_applications(),
|
|
case re:run(atom_to_list(App), "^emqx") of
|
|
{match, [{0, 4}]} -> true;
|
|
_ -> false
|
|
end
|
|
].
|
|
|
|
listeners(Listeners) ->
|
|
lists:filtermap(
|
|
fun
|
|
({_Protocol, #{bind := 0}}) ->
|
|
false;
|
|
({Protocol, Conf = #{}}) ->
|
|
{Conf1, Bind} = ip_port(Conf),
|
|
{true, {
|
|
listener_name(Protocol),
|
|
Protocol,
|
|
Bind,
|
|
ranch_opts(Conf1),
|
|
proto_opts(Conf1)
|
|
}}
|
|
end,
|
|
maps:to_list(Listeners)
|
|
).
|
|
|
|
list_listeners() ->
|
|
listeners(listeners()).
|
|
|
|
ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts).
|
|
|
|
ip_port(error, Opts) -> {Opts#{port => 18083}, 18083};
|
|
ip_port({Port, Opts}, _) when is_integer(Port) -> {Opts#{port => Port}, Port};
|
|
ip_port({{IP, Port}, Opts}, _) -> {Opts#{port => Port, ip => IP}, {IP, Port}}.
|
|
|
|
ranch_opts(Options) ->
|
|
Keys = [
|
|
handshake_timeout,
|
|
connection_type,
|
|
max_connections,
|
|
num_acceptors,
|
|
shutdown,
|
|
socket
|
|
],
|
|
RanchOpts = maps:with(Keys, Options),
|
|
SocketOpts = maps:fold(
|
|
fun filter_false/3,
|
|
[],
|
|
maps:without([inet6, ipv6_v6only, proxy_header | Keys], Options)
|
|
),
|
|
InetOpts =
|
|
case Options of
|
|
#{inet6 := true, ipv6_v6only := true} ->
|
|
[inet6, {ipv6_v6only, true}];
|
|
#{inet6 := true, ipv6_v6only := false} ->
|
|
[inet6];
|
|
_ ->
|
|
[inet]
|
|
end,
|
|
RanchOpts#{socket_opts => InetOpts ++ SocketOpts}.
|
|
|
|
proto_opts(#{proxy_header := ProxyHeader}) ->
|
|
#{proxy_header => ProxyHeader};
|
|
proto_opts(_Opts) ->
|
|
#{}.
|
|
|
|
filter_false(_K, false, S) -> S;
|
|
filter_false(K, V, S) -> [{K, V} | S].
|
|
|
|
listener_name(Protocol) ->
|
|
list_to_atom(atom_to_list(Protocol) ++ ":dashboard").
|
|
|
|
authorize(Req) ->
|
|
case cowboy_req:parse_header(<<"authorization">>, Req) of
|
|
{basic, Username, Password} ->
|
|
api_key_authorize(Req, Username, Password);
|
|
{bearer, Token} ->
|
|
case emqx_dashboard_admin:verify_token(Token) of
|
|
ok ->
|
|
ok;
|
|
{error, token_timeout} ->
|
|
{401, 'TOKEN_TIME_OUT', <<"Token expired, get new token by POST /login">>};
|
|
{error, not_found} ->
|
|
{401, 'BAD_TOKEN', <<"Get a token by POST /login">>}
|
|
end;
|
|
_ ->
|
|
return_unauthorized(
|
|
<<"AUTHORIZATION_HEADER_ERROR">>,
|
|
<<"Support authorization: basic/bearer ">>
|
|
)
|
|
end.
|
|
|
|
return_unauthorized(Code, Message) ->
|
|
{401,
|
|
#{
|
|
<<"WWW-Authenticate">> =>
|
|
<<"Basic Realm=\"emqx-dashboard\"">>
|
|
},
|
|
#{code => Code, message => Message}}.
|
|
|
|
listeners() ->
|
|
emqx_conf:get([dashboard, listeners], #{}).
|
|
|
|
api_key_authorize(Req, Key, Secret) ->
|
|
Path = cowboy_req:path(Req),
|
|
case emqx_mgmt_auth:authorize(Path, Key, Secret) of
|
|
ok ->
|
|
ok;
|
|
{error, <<"not_allowed">>} ->
|
|
return_unauthorized(
|
|
?BAD_API_KEY_OR_SECRET,
|
|
<<"Not allowed, Check api_key/api_secret">>
|
|
);
|
|
{error, _} ->
|
|
return_unauthorized(
|
|
?BAD_API_KEY_OR_SECRET,
|
|
<<"Check api_key/api_secret">>
|
|
)
|
|
end.
|
|
|
|
ensure_ssl_cert(Listeners = #{https := Https0 = #{ssl_options := SslOpts}}) ->
|
|
SslOpt1 = maps:from_list(emqx_tls_lib:to_server_opts(tls, SslOpts)),
|
|
Https1 = maps:remove(ssl_options, Https0),
|
|
Listeners#{https => maps:merge(Https1, SslOpt1)};
|
|
ensure_ssl_cert(Listeners) ->
|
|
Listeners.
|
|
|
|
dispatch() ->
|
|
static_dispatch() ++ dynamic_dispatch().
|
|
|
|
static_dispatch() ->
|
|
StaticFiles = ["/editor.worker.js", "/json.worker.js", "/version"],
|
|
[
|
|
{"/", cowboy_static, {priv_file, emqx_dashboard, "www/index.html"}},
|
|
{"/static/[...]", cowboy_static, {priv_dir, emqx_dashboard, "www/static"}}
|
|
] ++
|
|
[{Path, cowboy_static, {priv_file, emqx_dashboard, "www" ++ Path}} || Path <- StaticFiles].
|
|
|
|
dynamic_dispatch() ->
|
|
[
|
|
{emqx_mgmt_api_status:path(), emqx_mgmt_api_status, []},
|
|
{'_', emqx_dashboard_not_found, []}
|
|
].
|
|
|
|
emqx_api_name() ->
|
|
emqx_release:description() ++ " API".
|
|
|
|
emqx_release_version() ->
|
|
emqx_release:version().
|