1 |
|
-module(mongoose_admin_api). |
2 |
|
|
3 |
|
-behaviour(mongoose_http_handler). |
4 |
|
|
5 |
|
%% mongoose_http_handler callbacks |
6 |
|
-export([config_spec/0, routes/1]). |
7 |
|
|
8 |
|
%% config processing callbacks |
9 |
|
-export([process_config/1]). |
10 |
|
|
11 |
|
%% Utilities for the handler modules |
12 |
|
-export([init/2, |
13 |
|
is_authorized/2, |
14 |
|
parse_body/1, |
15 |
|
parse_qs/1, |
16 |
|
try_handle_request/3, |
17 |
|
throw_error/2, |
18 |
|
resource_created/4, |
19 |
|
respond/3]). |
20 |
|
|
21 |
|
-include("mongoose.hrl"). |
22 |
|
-include("mongoose_config_spec.hrl"). |
23 |
|
|
24 |
|
-type handler_options() :: #{path := string(), username => binary(), password => binary(), |
25 |
|
atom() => any()}. |
26 |
|
-type req() :: cowboy_req:req(). |
27 |
|
-type state() :: #{atom() => any()}. |
28 |
|
-type error_type() :: bad_request | denied | not_found | duplicate | internal. |
29 |
|
|
30 |
|
-export_type([state/0]). |
31 |
|
|
32 |
|
-callback routes(state()) -> mongoose_http_handler:routes(). |
33 |
|
|
34 |
|
%% mongoose_http_handler callbacks |
35 |
|
|
36 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
37 |
|
config_spec() -> |
38 |
104 |
Handlers = all_handlers(), |
39 |
104 |
#section{items = #{<<"username">> => #option{type = binary}, |
40 |
|
<<"password">> => #option{type = binary}, |
41 |
|
<<"handlers">> => #list{items = #option{type = atom, |
42 |
|
validate = {enum, Handlers}}, |
43 |
|
validate = unique}}, |
44 |
|
defaults = #{<<"handlers">> => Handlers}, |
45 |
|
process = fun ?MODULE:process_config/1}. |
46 |
|
|
47 |
|
-spec process_config(handler_options()) -> handler_options(). |
48 |
|
process_config(Opts) -> |
49 |
104 |
case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of |
50 |
|
true -> |
51 |
104 |
Opts; |
52 |
|
false -> |
53 |
:-( |
error(#{what => both_username_and_password_required, opts => Opts}) |
54 |
|
end. |
55 |
|
|
56 |
|
-spec routes(handler_options()) -> mongoose_http_handler:routes(). |
57 |
|
routes(Opts = #{path := BasePath}) -> |
58 |
111 |
[{[BasePath, Path], Module, ModuleOpts} || {Path, Module, ModuleOpts} <- api_paths(Opts)]. |
59 |
|
|
60 |
|
all_handlers() -> |
61 |
104 |
[contacts, users, sessions, messages, stanzas, muc_light, muc, inbox, domain, metrics]. |
62 |
|
|
63 |
|
-spec api_paths(handler_options()) -> mongoose_http_handler:routes(). |
64 |
|
api_paths(#{handlers := Handlers} = Opts) -> |
65 |
111 |
State = maps:with([username, password], Opts), |
66 |
111 |
lists:flatmap(fun(Handler) -> api_paths_for_handler(Handler, State) end, Handlers). |
67 |
|
|
68 |
|
api_paths_for_handler(Handler, State) -> |
69 |
1110 |
HandlerModule = list_to_existing_atom("mongoose_admin_api_" ++ atom_to_list(Handler)), |
70 |
1110 |
HandlerModule:routes(State). |
71 |
|
|
72 |
|
%% Utilities for the handler modules |
73 |
|
|
74 |
|
-spec init(req(), state()) -> {cowboy_rest, req(), state()}. |
75 |
|
init(Req, State) -> |
76 |
863 |
{cowboy_rest, Req, State}. |
77 |
|
|
78 |
|
-spec is_authorized(req(), state()) -> {true | {false, iodata()}, req(), state()}. |
79 |
|
is_authorized(Req, State) -> |
80 |
863 |
AuthDetails = mongoose_api_common:get_auth_details(Req), |
81 |
863 |
case authorize(State, AuthDetails) of |
82 |
|
true -> |
83 |
846 |
{true, Req, State}; |
84 |
|
false -> |
85 |
17 |
mongoose_api_common:make_unauthorized_response(Req, State) |
86 |
|
end. |
87 |
|
|
88 |
|
authorize(#{username := Username, password := Password}, AuthDetails) -> |
89 |
62 |
case AuthDetails of |
90 |
|
{AuthMethod, Username, Password} -> |
91 |
51 |
mongoose_api_common:is_known_auth_method(AuthMethod); |
92 |
|
_ -> |
93 |
11 |
false |
94 |
|
end; |
95 |
|
authorize(#{}, AuthDetails) -> |
96 |
801 |
AuthDetails =:= undefined. % Do not accept basic auth when not configured |
97 |
|
|
98 |
|
-spec parse_body(req()) -> #{atom() => jiffy:json_value()}. |
99 |
|
parse_body(Req) -> |
100 |
199 |
try |
101 |
199 |
{ok, Body, _Req2} = cowboy_req:read_body(Req), |
102 |
199 |
{DecodedBody} = jiffy:decode(Body), |
103 |
186 |
maps:from_list([{binary_to_existing_atom(K), V} || {K, V} <- DecodedBody]) |
104 |
|
catch Class:Reason:Stacktrace -> |
105 |
13 |
?LOG_WARNING(#{what => parse_body_failed, |
106 |
:-( |
class => Class, reason => Reason, stacktrace => Stacktrace}), |
107 |
13 |
throw_error(bad_request, <<"Invalid request body">>) |
108 |
|
end. |
109 |
|
|
110 |
|
-spec parse_qs(req()) -> #{atom() => binary() | true}. |
111 |
|
parse_qs(Req) -> |
112 |
11 |
try |
113 |
11 |
maps:from_list([{binary_to_existing_atom(K), V} || {K, V} <- cowboy_req:parse_qs(Req)]) |
114 |
|
catch Class:Reason:Stacktrace -> |
115 |
1 |
?LOG_WARNING(#{what => parse_qs_failed, |
116 |
:-( |
class => Class, reason => Reason, stacktrace => Stacktrace}), |
117 |
1 |
throw_error(bad_request, <<"Invalid query string">>) |
118 |
|
end. |
119 |
|
|
120 |
|
-spec try_handle_request(req(), state(), fun((req(), state()) -> Result)) -> Result. |
121 |
|
try_handle_request(Req, State, F) -> |
122 |
844 |
try |
123 |
844 |
F(Req, State) |
124 |
|
catch throw:#{error_type := ErrorType, message := Msg} -> |
125 |
179 |
error_response(ErrorType, Msg, Req, State) |
126 |
|
end. |
127 |
|
|
128 |
|
-spec throw_error(error_type(), iodata()) -> no_return(). |
129 |
|
throw_error(ErrorType, Msg) -> |
130 |
179 |
throw(#{error_type => ErrorType, message => Msg}). |
131 |
|
|
132 |
|
-spec resource_created(req(), state(), iodata(), iodata()) -> {stop, req(), state()}. |
133 |
|
resource_created(Req, State, Path, Body) -> |
134 |
10 |
Req2 = cowboy_req:set_resp_body(Body, Req), |
135 |
10 |
Headers = #{<<"location">> => Path}, |
136 |
10 |
Req3 = cowboy_req:reply(201, Headers, Req2), |
137 |
10 |
{stop, Req3, State}. |
138 |
|
|
139 |
|
%% @doc Send response when it can't be returned in a tuple from the handler (e.g. for DELETE) |
140 |
|
-spec respond(req(), state(), jiffy:json_value()) -> {stop, req(), state()}. |
141 |
|
respond(Req, State, Response) -> |
142 |
2 |
Req2 = cowboy_req:set_resp_body(jiffy:encode(Response), Req), |
143 |
2 |
Req3 = cowboy_req:reply(200, Req2), |
144 |
2 |
{stop, Req3, State}. |
145 |
|
|
146 |
|
-spec error_response(error_type(), iodata(), req(), state()) -> {stop, req(), state()}. |
147 |
|
error_response(ErrorType, Message, Req, State) -> |
148 |
179 |
BinMessage = iolist_to_binary(Message), |
149 |
179 |
?LOG(log_level(ErrorType), #{what => mongoose_admin_api_error_response, |
150 |
|
error_type => ErrorType, |
151 |
|
message => BinMessage, |
152 |
:-( |
req => Req}), |
153 |
179 |
Req1 = cowboy_req:reply(error_code(ErrorType), #{}, jiffy:encode(BinMessage), Req), |
154 |
179 |
{stop, Req1, State}. |
155 |
|
|
156 |
|
-spec error_code(error_type()) -> non_neg_integer(). |
157 |
101 |
error_code(bad_request) -> 400; |
158 |
30 |
error_code(denied) -> 403; |
159 |
46 |
error_code(not_found) -> 404; |
160 |
2 |
error_code(duplicate) -> 409; |
161 |
:-( |
error_code(internal) -> 500. |
162 |
|
|
163 |
|
-spec log_level(error_type()) -> logger:level(). |
164 |
202 |
log_level(bad_request) -> warning; |
165 |
60 |
log_level(denied) -> warning; |
166 |
92 |
log_level(not_found) -> warning; |
167 |
4 |
log_level(duplicate) -> warning; |
168 |
:-( |
log_level(internal) -> error. |