./ct_report/coverage/mongoose_api_common.COVER.html

1 %%%-------------------------------------------------------------------
2 %%% @author ludwikbukowski
3 %%% @copyright (C) 2016, Erlang Solutions Ltd.
4 %%% Created : 20. Jul 2016 10:16
5 %%%-------------------------------------------------------------------
6
7 %% @doc MongooseIM REST API backend
8 %%
9 %% This module handles the client HTTP REST requests, then respectively convert
10 %% them to Commands from mongoose_commands and execute with `admin` privileges.
11 %% It supports responses with appropriate HTTP Status codes returned to the
12 %% client.
13 %% This module implements behaviour introduced in ejabberd_cowboy which is
14 %% %% built on top of the cowboy library.
15 %% The method supported: GET, POST, PUT, DELETE. Only JSON format.
16 %% The library "jiffy" used to serialize and deserialized JSON data.
17 %%
18 %% REQUESTS
19 %%
20 %% The module is based on mongoose_commands registry.
21 %% The root http path for a command is build based on the "category" field.
22 %% %% It's always used as path a prefix.
23 %% The commands are translated to HTTP API in the following manner:
24 %%
25 %% command of action "read" will be called by GET request
26 %% command of action "create" will be called by POST request
27 %% command of action "update" will be called by PUT request
28 %% command of action "delete" will be called by DELETE request
29 %%
30 %% The args of the command will be filled with the values provided in path
31 %% %% bindings or body parameters, depending of the method type:
32 %% - for command of action "read" or "delete" all the args are pulled from the
33 %% path bindings. The path should be constructed of pairs "/arg_name/arg_value"
34 %% so that it could match the {arg_name, type} %% pattern in the command
35 %% registry. E.g having the record of category "users" and args:
36 %% [{username, binary}, {domain, binary}] we will have to make following GET
37 %% request %% path: http://domain:port/api/users/username/Joe/domain/localhost
38 %% and the command will be called with arguments "Joe" and "localhost"
39 %%
40 %% - for command of action "create" or "update" args are pulled from the body
41 %% JSON, except those that are on the "identifiers" list of the command. Those
42 %% go to the path bindings.
43 %% E.g having the record of category "animals", action "update" and args:
44 %% [{species, binary}, {name, binary}, {age, integer}]
45 %% and identifiers:
46 %% [species, name]
47 %% we can set the age for our elephant Ed in the PUT request:
48 %% path: http://domain:port/api/species/elephant/name/Ed
49 %% body: {"age" : "10"}
50 %% and then the command will be called with arguments ["elephant", "Ed" and 10].
51 %%
52 %% RESPONSES
53 %%
54 %% The API supports some of the http status code like 200, 201, 400, 404 etc
55 %% depending on the return value of the command execution and arguments checks.
56 %% Additionally, when the command's action is "create" and it returns a value,
57 %% it is concatenated to the path and return to the client in header "location"
58 %% with response code 201 so that it could represent now a new created resource.
59 %% If error occured while executing the command, the appropriate reason is
60 %% returned in response body.
61
62 -module(mongoose_api_common).
63 -author("ludwikbukowski").
64 -include("mongoose_api.hrl").
65 -include("mongoose.hrl").
66
67 %% API
68 -export([create_admin_url_path/1,
69 create_user_url_path/1,
70 action_to_method/1,
71 method_to_action/1,
72 parse_request_body/1,
73 get_allowed_methods/1,
74 process_request/4,
75 reload_dispatches/1,
76 get_auth_details/1,
77 is_known_auth_method/1,
78 error_response/4,
79 make_unauthorized_response/2,
80 check_password/2]).
81
82 -ignore_xref([reload_dispatches/1]).
83
84 %% @doc Reload all ejabberd_cowboy listeners.
85 %% When a command is registered or unregistered, the routing paths that
86 %% cowboy stores as a "dispatch" must be refreshed.
87 %% Read more http://ninenines.eu/docs/en/cowboy/1.0/guide/routing/
88 reload_dispatches(drop) ->
89
:-(
drop;
90 reload_dispatches(_Command) ->
91 8354 Listeners = supervisor:which_children(ejabberd_listeners),
92 8354 CowboyListeners = [Child || {_Id, Child, _Type, [ejabberd_cowboy]} <- Listeners],
93 8354 [ejabberd_cowboy:reload_dispatch(Child) || Child <- CowboyListeners],
94 8354 drop.
95
96
97 -spec create_admin_url_path(mongoose_commands:t()) -> ejabberd_cowboy:path().
98 create_admin_url_path(Command) ->
99 3172 iolist_to_binary(create_admin_url_path_iodata(Command)).
100
101 create_admin_url_path_iodata(Command) ->
102 3172 ["/", mongoose_commands:category(Command),
103 maybe_add_bindings(Command, admin), maybe_add_subcategory(Command)].
104
105 -spec create_user_url_path(mongoose_commands:t()) -> ejabberd_cowboy:path().
106 create_user_url_path(Command) ->
107
:-(
iolist_to_binary(create_user_url_path_iodata(Command)).
108
109 create_user_url_path_iodata(Command) ->
110
:-(
["/", mongoose_commands:category(Command), maybe_add_bindings(Command, user)].
111
112 -spec process_request(Method :: method(),
113 Command :: mongoose_commands:t(),
114 Req :: cowboy_req:req() | {list(), cowboy_req:req()},
115 State :: http_api_state()) ->
116 {any(), cowboy_req:req(), http_api_state()}.
117 process_request(Method, Command, Req, #http_api_state{bindings = Binds, entity = Entity} = State)
118 when ((Method == <<"POST">>) or (Method == <<"PUT">>)) ->
119 58 {Params, Req2} = Req,
120 58 QVals = cowboy_req:parse_qs(Req2),
121 58 QV = [{binary_to_existing_atom(K, utf8), V} || {K, V} <- QVals],
122 58 Params2 = Binds ++ Params ++ QV ++ maybe_add_caller(Entity),
123 58 handle_request(Method, Command, Params2, Req2, State);
124 process_request(Method, Command, Req, #http_api_state{bindings = Binds, entity = Entity}=State)
125 when ((Method == <<"GET">>) or (Method == <<"DELETE">>)) ->
126 43 QVals = cowboy_req:parse_qs(Req),
127 43 QV = [{binary_to_existing_atom(K, utf8), V} || {K, V} <- QVals],
128 43 BindsAndVars = Binds ++ QV ++ maybe_add_caller(Entity),
129 43 handle_request(Method, Command, BindsAndVars, Req, State).
130
131 -spec handle_request(Method :: method(),
132 Command :: mongoose_commands:t(),
133 Args :: args_applied(),
134 Req :: cowboy_req:req(),
135 State :: http_api_state()) ->
136 {any(), cowboy_req:req(), http_api_state()}.
137 handle_request(Method, Command, Args, Req, #http_api_state{entity = Entity} = State) ->
138 101 case check_and_extract_args(mongoose_commands:args(Command),
139 mongoose_commands:optargs(Command), Args) of
140 {error, Type, Reason} ->
141 1 handle_result(Method, {error, Type, Reason}, Req, State);
142 ConvertedArgs ->
143 100 handle_result(Method,
144 execute_command(ConvertedArgs, Command, Entity),
145 Req, State)
146 end.
147
148 -type correct_result() :: mongoose_commands:success().
149 -type error_result() :: mongoose_commands:failure().
150
151 -spec handle_result(Method, Result, Req, State) -> Return when
152 Method :: method() | no_call,
153 Result :: correct_result() | error_result(),
154 Req :: cowboy_req:req(),
155 State :: http_api_state(),
156 Return :: {any(), cowboy_req:req(), http_api_state()}.
157 handle_result(Verb, ok, Req, State) ->
158 26 handle_result(Verb, {ok, nocontent}, Req, State);
159 handle_result(<<"GET">>, {ok, Result}, Req, State) ->
160 32 {jiffy:encode(Result), Req, State};
161 handle_result(<<"POST">>, {ok, nocontent}, Req, State) ->
162 14 Req2 = cowboy_req:reply(204, Req),
163 14 {stop, Req2, State};
164 handle_result(<<"POST">>, {ok, Res}, Req, State) ->
165 7 Path = iolist_to_binary(cowboy_req:uri(Req)),
166 7 Req2 = cowboy_req:set_resp_body(Res, Req),
167 7 Req3 = maybe_add_location_header(Res, binary_to_list(Path), Req2),
168 7 {stop, Req3, State};
169 %% Ignore the returned value from a command for DELETE methods
170 handle_result(<<"DELETE">>, {ok, _Res}, Req, State) ->
171 6 Req2 = cowboy_req:reply(204, Req),
172 6 {stop, Req2, State};
173 handle_result(<<"PUT">>, {ok, nocontent}, Req, State) ->
174 8 Req2 = cowboy_req:reply(204, Req),
175 8 {stop, Req2, State};
176 handle_result(<<"PUT">>, {ok, Res}, Req, State) ->
177 2 Req2 = cowboy_req:set_resp_body(Res, Req),
178 2 Req3 = cowboy_req:reply(201, Req2),
179 2 {stop, Req3, State};
180 handle_result(_, {error, Error, Reason}, Req, State) ->
181 32 error_response(Error, Reason, Req, State);
182 handle_result(no_call, _, Req, State) ->
183
:-(
error_response(not_implemented, <<>>, Req, State).
184
185
186 -spec parse_request_body(any()) -> {args_applied(), cowboy_req:req()} | {error, any()}.
187 parse_request_body(Req) ->
188 58 {ok, Body, Req2} = cowboy_req:read_body(Req),
189 58 {Data} = jiffy:decode(Body),
190 58 try
191 58 Params = create_params_proplist(Data),
192 58 {Params, Req2}
193 catch
194 Class:Reason:StackTrace ->
195
:-(
?LOG_ERROR(#{what => parse_request_body_failed, class => Class,
196
:-(
reason => Reason, stacktrace => StackTrace}),
197
:-(
{error, Reason}
198 end.
199
200 %% @doc Checks if the arguments are correct. Return the arguments that can be applied to the
201 %% execution of command.
202 -spec check_and_extract_args(arg_spec_list(), optarg_spec_list(), args_applied()) ->
203 map() | {error, atom(), any()}.
204 check_and_extract_args(ReqArgs, OptArgs, RequestArgList) ->
205 101 try
206 101 AllArgs = ReqArgs ++ [{N, T} || {N, T, _} <- OptArgs],
207 101 AllArgVals = [{N, T, proplists:get_value(N, RequestArgList)} || {N, T} <- AllArgs],
208 101 ConvArgs = [{N, convert_arg(T, V)} || {N, T, V} <- AllArgVals, V =/= undefined],
209 100 maps:from_list(ConvArgs)
210 catch
211 Class:Reason:StackTrace ->
212 1 ?LOG_ERROR(#{what => check_and_extract_args_failed, class => Class,
213
:-(
reason => Reason, stacktrace => StackTrace}),
214 1 {error, bad_request, Reason}
215 end.
216
217 -spec execute_command(mongoose_commands:args(),
218 mongoose_commands:t(),
219 mongoose_commands:caller()) ->
220 correct_result() | error_result().
221 execute_command(ArgMap, Command, Entity) ->
222 100 mongoose_commands:execute(Entity, mongoose_commands:name(Command), ArgMap).
223
224 -spec maybe_add_caller(admin | binary()) -> list() | list({caller, binary()}).
225 maybe_add_caller(admin) ->
226 101 [];
227 maybe_add_caller(JID) ->
228
:-(
[{caller, JID}].
229
230 -spec maybe_add_location_header(binary() | list(), list(), any())
231 -> cowboy_req:req().
232 maybe_add_location_header(Result, ResourcePath, Req) when is_binary(Result) ->
233 7 add_location_header(binary_to_list(Result), ResourcePath, Req);
234 maybe_add_location_header(Result, ResourcePath, Req) when is_list(Result) ->
235
:-(
add_location_header(Result, ResourcePath, Req);
236 maybe_add_location_header(_, _Path, Req) ->
237
:-(
cowboy_req:reply(204, #{}, Req).
238
239 add_location_header(Result, ResourcePath, Req) ->
240 7 Path = [ResourcePath, "/", Result],
241 7 Headers = #{<<"location">> => Path},
242 7 cowboy_req:reply(201, Headers, Req).
243
244 -spec convert_arg(atom(), any()) -> boolean() | integer() | float() | binary() | string() | {error, bad_type}.
245 convert_arg(binary, Binary) when is_binary(Binary) ->
246 248 Binary;
247 convert_arg(boolean, Value) when is_boolean(Value) ->
248 1 Value;
249 convert_arg(integer, Binary) when is_binary(Binary) ->
250
:-(
binary_to_integer(Binary);
251 convert_arg(integer, Integer) when is_integer(Integer) ->
252
:-(
Integer;
253 convert_arg(float, Binary) when is_binary(Binary) ->
254
:-(
binary_to_float(Binary);
255 convert_arg(float, Float) when is_float(Float) ->
256
:-(
Float;
257 convert_arg([Type], List) when is_list(List) ->
258
:-(
[ convert_arg(Type, Item) || Item <- List ];
259 convert_arg(_, _Binary) ->
260 1 throw({error, bad_type}).
261
262 -spec create_params_proplist(list({binary(), binary()})) -> args_applied().
263 create_params_proplist(ArgList) ->
264 58 lists:sort([{to_atom(Arg), Value} || {Arg, Value} <- ArgList]).
265
266 %% @doc Returns list of allowed methods.
267 -spec get_allowed_methods(admin | user) -> list(method()).
268 get_allowed_methods(Entity) ->
269 103 Commands = mongoose_commands:list(Entity),
270 103 [action_to_method(mongoose_commands:action(Command)) || Command <- Commands].
271
272 -spec maybe_add_bindings(mongoose_commands:t(), admin|user) -> iolist().
273 maybe_add_bindings(Command, Entity) ->
274 3172 Action = mongoose_commands:action(Command),
275 3172 Args = mongoose_commands:args(Command),
276 3172 BindAndBody = both_bind_and_body(Action),
277 3172 case BindAndBody of
278 true ->
279 1806 Ids = mongoose_commands:identifiers(Command),
280 1806 Bindings = [El || {Key, _Value} = El <- Args, true =:= proplists:is_defined(Key, Ids)],
281 1806 add_bindings(Bindings, Entity);
282 false ->
283 1366 add_bindings(Args, Entity)
284 end.
285
286 maybe_add_subcategory(Command) ->
287 3172 SubCategory = mongoose_commands:subcategory(Command),
288 3172 case SubCategory of
289 undefined ->
290 2182 [];
291 _ ->
292 990 ["/", SubCategory]
293 end.
294
295 -spec both_bind_and_body(mongoose_commands:action()) -> boolean().
296 both_bind_and_body(update) ->
297 589 true;
298 both_bind_and_body(create) ->
299 1217 true;
300 both_bind_and_body(read) ->
301 666 false;
302 both_bind_and_body(delete) ->
303 700 false.
304
305 add_bindings(Args, Entity) ->
306 3172 [add_bind(A, Entity) || A <- Args].
307
308 %% skip "caller" arg for frontend command
309 add_bind({caller, _}, user) ->
310
:-(
"";
311 add_bind({ArgName, _}, _Entity) ->
312 4635 lists:flatten(["/:", atom_to_list(ArgName)]);
313 add_bind(Other, _) ->
314
:-(
throw({error, bad_arg_spec, Other}).
315
316 -spec to_atom(binary() | atom()) -> atom().
317 to_atom(Bin) when is_binary(Bin) ->
318 116 erlang:binary_to_existing_atom(Bin, utf8);
319 to_atom(Atom) when is_atom(Atom) ->
320
:-(
Atom.
321
322 %%--------------------------------------------------------------------
323 %% HTTP utils
324 %%--------------------------------------------------------------------
325 %%-spec error_response(mongoose_commands:errortype(), any(), http_api_state()) ->
326 %% {stop, any(), http_api_state()}.
327 %%error_response(ErrorType, Req, State) ->
328 %% error_response(ErrorType, <<>>, Req, State).
329
330 -spec error_response(mongoose_commands:errortype(), mongoose_commands:errorreason(), cowboy_req:req(), http_api_state()) ->
331 {stop, cowboy_req:req(), http_api_state()}.
332 error_response(ErrorType, Reason, Req, State) ->
333 33 BinReason = case Reason of
334 4 B when is_binary(B) -> B;
335 28 L when is_list(L) -> list_to_binary(L);
336 1 Other -> list_to_binary(io_lib:format("~p", [Other]))
337 end,
338 33 ?LOG_ERROR(#{what => rest_common_error,
339
:-(
error_type => ErrorType, reason => Reason, req => Req}),
340 33 Req1 = cowboy_req:reply(error_code(ErrorType), #{}, BinReason, Req),
341 33 {stop, Req1, State}.
342
343 %% HTTP status codes
344 4 error_code(denied) -> 403;
345
:-(
error_code(not_implemented) -> 501;
346 9 error_code(bad_request) -> 400;
347 12 error_code(type_error) -> 400;
348 7 error_code(not_found) -> 404;
349 1 error_code(internal) -> 500;
350 error_code(Other) ->
351
:-(
?WARNING_MSG("Unknown error identifier \"~p\". See mongoose_commands:errortype() for allowed values.", [Other]),
352
:-(
500.
353
354 727 action_to_method(read) -> <<"GET">>;
355 661 action_to_method(update) -> <<"PUT">>;
356 782 action_to_method(delete) -> <<"DELETE">>;
357 1287 action_to_method(create) -> <<"POST">>;
358
:-(
action_to_method(_) -> undefined.
359
360 33 method_to_action(<<"GET">>) -> read;
361 39 method_to_action(<<"POST">>) -> create;
362 19 method_to_action(<<"PUT">>) -> update;
363 11 method_to_action(<<"DELETE">>) -> delete.
364
365 %%--------------------------------------------------------------------
366 %% Authorization
367 %%--------------------------------------------------------------------
368
369 -spec get_auth_details(cowboy_req:req()) ->
370 {basic, User :: binary(), Password :: binary()} | undefined.
371 get_auth_details(Req) ->
372 202 case cowboy_req:parse_header(<<"authorization">>, Req) of
373 {basic, _User, _Password} = Details ->
374 100 Details;
375 _ ->
376 102 undefined
377 end.
378
379 -spec is_known_auth_method(atom()) -> boolean().
380 98 is_known_auth_method(basic) -> true;
381
:-(
is_known_auth_method(_) -> false.
382
383 make_unauthorized_response(Req, State) ->
384 3 {{false, <<"Basic realm=\"mongooseim\"">>}, Req, State}.
385
386 -spec check_password(jid:jid() | error, binary()) -> {true, mongoose_credentials:t()} | false.
387 check_password(error, _) ->
388
:-(
false;
389 check_password(JID, Password) ->
390 146 {LUser, LServer} = jid:to_lus(JID),
391 146 case mongoose_domain_api:get_domain_host_type(LServer) of
392 {ok, HostType} ->
393 146 Creds0 = mongoose_credentials:new(LServer, HostType),
394 146 Creds1 = mongoose_credentials:set(Creds0, username, LUser),
395 146 Creds2 = mongoose_credentials:set(Creds1, password, Password),
396 146 case ejabberd_auth:authorize(Creds2) of
397 146 {ok, Creds} -> {true, Creds};
398
:-(
_ -> false
399 end;
400
:-(
{error, not_found} -> false
401 end.
Line Hits Source