1 |
|
%%%------------------------------------------------------------------- |
2 |
|
%%% @author ludwikbukowski |
3 |
|
%%% @copyright (C) 2016, Erlang Solutions Ltd. |
4 |
|
%%% Created : 05. Jul 2016 12:59 |
5 |
|
%%%------------------------------------------------------------------- |
6 |
|
|
7 |
|
%% @doc MongooseIM REST HTTP API for administration. |
8 |
|
%% This module implements cowboy REST callbacks and |
9 |
|
%% passes the requests on to the http api backend module. |
10 |
|
%% @end |
11 |
|
-module(mongoose_api_admin). |
12 |
|
-author("ludwikbukowski"). |
13 |
|
|
14 |
|
-behaviour(mongoose_http_handler). |
15 |
|
-behaviour(cowboy_rest). |
16 |
|
|
17 |
|
%% mongoose_http_handler callbacks |
18 |
|
-export([config_spec/0, routes/1]). |
19 |
|
|
20 |
|
%% config processing callbacks |
21 |
|
-export([process_config/1]). |
22 |
|
|
23 |
|
%% cowboy_rest exports |
24 |
|
-export([allowed_methods/2, |
25 |
|
content_types_provided/2, |
26 |
|
terminate/3, |
27 |
|
init/2, |
28 |
|
options/2, |
29 |
|
content_types_accepted/2, |
30 |
|
delete_resource/2, |
31 |
|
is_authorized/2]). |
32 |
|
%% local callbacks |
33 |
|
-export([to_json/2, from_json/2]). |
34 |
|
|
35 |
|
-ignore_xref([cowboy_router_paths/2, from_json/2, to_json/2]). |
36 |
|
|
37 |
|
-include("mongoose_api.hrl"). |
38 |
|
-include("mongoose.hrl"). |
39 |
|
-include("mongoose_config_spec.hrl"). |
40 |
|
|
41 |
|
-import(mongoose_api_common, [error_response/4, |
42 |
|
action_to_method/1, |
43 |
|
method_to_action/1, |
44 |
|
error_code/1, |
45 |
|
process_request/4, |
46 |
|
parse_request_body/1]). |
47 |
|
|
48 |
|
-type credentials() :: {Username :: binary(), Password :: binary()} | any. |
49 |
|
|
50 |
|
-type handler_options() :: #{path := string(), username => binary(), password => binary(), |
51 |
|
atom() => any()}. |
52 |
|
|
53 |
|
%%-------------------------------------------------------------------- |
54 |
|
%% mongoose_http_handler callbacks |
55 |
|
%%-------------------------------------------------------------------- |
56 |
|
|
57 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
58 |
|
config_spec() -> |
59 |
83 |
#section{items = #{<<"username">> => #option{type = binary}, |
60 |
|
<<"password">> => #option{type = binary}}, |
61 |
|
process = fun ?MODULE:process_config/1}. |
62 |
|
|
63 |
|
-spec process_config(handler_options()) -> handler_options(). |
64 |
|
process_config(Opts) -> |
65 |
83 |
case maps:is_key(username, Opts) =:= maps:is_key(password, Opts) of |
66 |
|
true -> |
67 |
83 |
Opts; |
68 |
|
false -> |
69 |
:-( |
error(#{what => both_username_and_password_required, opts => Opts}) |
70 |
|
end. |
71 |
|
|
72 |
|
-spec routes(handler_options()) -> mongoose_http_handler:routes(). |
73 |
|
routes(Opts = #{path := BasePath}) -> |
74 |
159 |
ejabberd_hooks:add(register_command, global, mongoose_api_common, reload_dispatches, 50), |
75 |
159 |
ejabberd_hooks:add(unregister_command, global, mongoose_api_common, reload_dispatches, 50), |
76 |
159 |
try |
77 |
159 |
Commands = mongoose_commands:list(admin), |
78 |
159 |
[handler_path(BasePath, Command, Opts) || Command <- Commands] |
79 |
|
catch |
80 |
|
Class:Err:StackTrace -> |
81 |
:-( |
?LOG_ERROR(#{what => getting_command_list_error, |
82 |
:-( |
class => Class, reason => Err, stacktrace => StackTrace}), |
83 |
:-( |
[] |
84 |
|
end. |
85 |
|
|
86 |
|
%%-------------------------------------------------------------------- |
87 |
|
%% cowboy_rest callbacks |
88 |
|
%%-------------------------------------------------------------------- |
89 |
|
|
90 |
|
init(Req, Opts) -> |
91 |
101 |
Bindings = maps:to_list(cowboy_req:bindings(Req)), |
92 |
101 |
#{command_category := CommandCategory, command_subcategory := CommandSubCategory} = Opts, |
93 |
101 |
Auth = auth_opts(Opts), |
94 |
101 |
State = #http_api_state{allowed_methods = mongoose_api_common:get_allowed_methods(admin), |
95 |
|
bindings = Bindings, |
96 |
|
command_category = CommandCategory, |
97 |
|
command_subcategory = CommandSubCategory, |
98 |
|
auth = Auth}, |
99 |
101 |
{cowboy_rest, Req, State}. |
100 |
|
|
101 |
2 |
auth_opts(#{username := UserName, password := Password}) -> {UserName, Password}; |
102 |
99 |
auth_opts(#{}) -> any. |
103 |
|
|
104 |
|
options(Req, State) -> |
105 |
:-( |
Req1 = set_cors_headers(Req), |
106 |
:-( |
{ok, Req1, State}. |
107 |
|
|
108 |
|
set_cors_headers(Req) -> |
109 |
:-( |
Req1 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Methods">>, |
110 |
|
<<"GET, OPTIONS, PUT, POST, DELETE">>, Req), |
111 |
:-( |
Req2 = cowboy_req:set_resp_header(<<"Access-Control-Allow-Origin">>, |
112 |
|
<<"*">>, Req1), |
113 |
:-( |
cowboy_req:set_resp_header(<<"Access-Control-Allow-Headers">>, |
114 |
|
<<"Content-Type">>, Req2). |
115 |
|
|
116 |
|
allowed_methods(Req, #http_api_state{command_category = Name} = State) -> |
117 |
101 |
CommandList = mongoose_commands:list(admin, Name), |
118 |
101 |
AllowedMethods = [action_to_method(mongoose_commands:action(Command)) |
119 |
101 |
|| Command <- CommandList], |
120 |
101 |
{[<<"OPTIONS">> | AllowedMethods], Req, State}. |
121 |
|
|
122 |
|
content_types_provided(Req, State) -> |
123 |
100 |
CTP = [{{<<"application">>, <<"json">>, '*'}, to_json}], |
124 |
100 |
{CTP, Req, State}. |
125 |
|
|
126 |
|
content_types_accepted(Req, State) -> |
127 |
58 |
CTA = [{{<<"application">>, <<"json">>, '*'}, from_json}], |
128 |
58 |
{CTA, Req, State}. |
129 |
|
|
130 |
|
terminate(_Reason, _Req, _State) -> |
131 |
101 |
ok. |
132 |
|
|
133 |
|
%% @doc Called for a method of type "DELETE" |
134 |
|
delete_resource(Req, #http_api_state{command_category = Category, |
135 |
|
command_subcategory = SubCategory, |
136 |
|
bindings = B} = State) -> |
137 |
9 |
Arity = length(B), |
138 |
9 |
Cmds = mongoose_commands:list(admin, Category, method_to_action(<<"DELETE">>), SubCategory), |
139 |
9 |
[Command] = [C || C <- Cmds, mongoose_commands:arity(C) == Arity], |
140 |
9 |
process_request(<<"DELETE">>, Command, Req, State). |
141 |
|
|
142 |
|
|
143 |
|
%%-------------------------------------------------------------------- |
144 |
|
%% Authorization |
145 |
|
%%-------------------------------------------------------------------- |
146 |
|
|
147 |
|
% @doc Cowboy callback |
148 |
|
is_authorized(Req, State) -> |
149 |
101 |
ControlCreds = get_control_creds(State), |
150 |
101 |
AuthDetails = mongoose_api_common:get_auth_details(Req), |
151 |
101 |
case authorize(ControlCreds, AuthDetails) of |
152 |
|
true -> |
153 |
100 |
{true, Req, State}; |
154 |
|
false -> |
155 |
1 |
mongoose_api_common:make_unauthorized_response(Req, State) |
156 |
|
end. |
157 |
|
|
158 |
|
-spec authorize(credentials(), {AuthMethod :: atom(), |
159 |
|
Username :: binary(), |
160 |
|
Password :: binary()}) -> boolean(). |
161 |
99 |
authorize(any, _) -> true; |
162 |
:-( |
authorize(_, undefined) -> false; |
163 |
|
authorize(ControlCreds, {AuthMethod, User, Password}) -> |
164 |
2 |
compare_creds(ControlCreds, {User, Password}) andalso |
165 |
1 |
mongoose_api_common:is_known_auth_method(AuthMethod). |
166 |
|
|
167 |
|
% @doc Checks if credentials are the same (if control creds are 'any' |
168 |
|
% it is equal to everything). |
169 |
|
-spec compare_creds(credentials(), credentials() | undefined) -> boolean(). |
170 |
1 |
compare_creds({User, Pass}, {User, Pass}) -> true; |
171 |
1 |
compare_creds(_, _) -> false. |
172 |
|
|
173 |
|
get_control_creds(#http_api_state{auth = Creds}) -> |
174 |
101 |
Creds. |
175 |
|
|
176 |
|
%%-------------------------------------------------------------------- |
177 |
|
%% Internal funs |
178 |
|
%%-------------------------------------------------------------------- |
179 |
|
|
180 |
|
%% @doc Called for a method of type "GET" |
181 |
|
to_json(Req, #http_api_state{command_category = Category, |
182 |
|
command_subcategory = SubCategory, |
183 |
|
bindings = B} = State) -> |
184 |
33 |
Cmds = mongoose_commands:list(admin, Category, method_to_action(<<"GET">>), SubCategory), |
185 |
33 |
Arity = length(B), |
186 |
33 |
case [C || C <- Cmds, mongoose_commands:arity(C) == Arity] of |
187 |
|
[Command] -> |
188 |
32 |
process_request(<<"GET">>, Command, Req, State); |
189 |
|
[] -> |
190 |
1 |
error_response(not_found, ?ARGS_LEN_ERROR, Req, State) |
191 |
|
end. |
192 |
|
|
193 |
|
%% @doc Called for a method of type "POST" and "PUT" |
194 |
|
from_json(Req, #http_api_state{command_category = Category, |
195 |
|
command_subcategory = SubCategory, |
196 |
|
bindings = B} = State) -> |
197 |
58 |
case parse_request_body(Req) of |
198 |
|
{error, _R}-> |
199 |
:-( |
error_response(bad_request, ?BODY_MALFORMED, Req, State); |
200 |
|
{Params, _} -> |
201 |
58 |
Method = cowboy_req:method(Req), |
202 |
58 |
Cmds = mongoose_commands:list(admin, Category, method_to_action(Method), SubCategory), |
203 |
58 |
QVals = cowboy_req:parse_qs(Req), |
204 |
58 |
Arity = length(B) + length(Params) + length(QVals), |
205 |
58 |
case [C || C <- Cmds, mongoose_commands:arity(C) == Arity] of |
206 |
|
[Command] -> |
207 |
58 |
process_request(Method, Command, {Params, Req}, State); |
208 |
|
[] -> |
209 |
:-( |
error_response(not_found, ?ARGS_LEN_ERROR, Req, State) |
210 |
|
end |
211 |
|
end. |
212 |
|
|
213 |
|
-spec handler_path(ejabberd_cowboy:path(), mongoose_commands:t(), handler_options()) -> |
214 |
|
ejabberd_cowboy:route(). |
215 |
|
handler_path(Base, Command, CommonOpts) -> |
216 |
3719 |
{[Base, mongoose_api_common:create_admin_url_path(Command)], |
217 |
|
?MODULE, CommonOpts#{command_category => mongoose_commands:category(Command), |
218 |
|
command_subcategory => mongoose_commands:subcategory(Command)}}. |