./ct_report/coverage/mongoose_api_admin.COVER.html

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