./ct_report/coverage/mongoose_domain_handler.COVER.html

1 %% REST API for domain actions.
2 -module(mongoose_domain_handler).
3 -behaviour(cowboy_rest).
4
5 %% ejabberd_cowboy exports
6 -export([cowboy_router_paths/2]).
7
8 %% Standard cowboy_rest callbacks.
9 -export([init/2,
10 allowed_methods/2,
11 content_types_accepted/2,
12 content_types_provided/2,
13 is_authorized/2,
14 delete_resource/2]).
15
16 %% Custom cowboy_rest callbacks.
17 -export([handle_domain/2,
18 to_json/2]).
19
20 -ignore_xref([cowboy_router_paths/2, handle_domain/2, to_json/2]).
21
22 -include("mongoose_logger.hrl").
23 -type state() :: map().
24
25 -spec cowboy_router_paths(ejabberd_cowboy:path(), ejabberd_cowboy:options()) ->
26 ejabberd_cowboy:implemented_result().
27 cowboy_router_paths(Base, Opts) ->
28 117 [{[Base, "/domains/:domain"], ?MODULE, Opts}].
29
30 %% cowboy_rest callbacks:
31 %% Opts could be `[{password, <<\"secret\">>}, {username, <<\"admin\">>}]'.
32 init(Req, Opts) ->
33 99 {cowboy_rest, Req, maps:from_list(Opts)}.
34
35 allowed_methods(Req, State) ->
36 99 {[<<"GET">>, <<"PUT">>, <<"PATCH">>, <<"DELETE">>], Req, State}.
37
38 content_types_accepted(Req, State) ->
39 56 {[{{<<"application">>, <<"json">>, '*'}, handle_domain}],
40 Req, State}.
41
42 content_types_provided(Req, State) ->
43 84 {[{{<<"application">>, <<"json">>, '*'}, to_json}], Req, State}.
44
45 is_authorized(Req, State) ->
46 99 HeaderDetails = cowboy_req:parse_header(<<"authorization">>, Req),
47 99 ConfigDetails = state_to_details(State),
48 99 case check_auth(HeaderDetails, ConfigDetails) of
49 ok ->
50 84 {true, Req, State};
51 {error, auth_header_passed_but_not_expected} ->
52 5 {false, reply_error(403, <<"basic auth provided, but not configured">>, Req), State};
53 {error, auth_password_invalid} ->
54 5 {false, reply_error(403, <<"basic auth provided, invalid password">>, Req), State};
55 {error, no_basic_auth_provided} ->
56 5 {false, reply_error(403, <<"basic auth is required">>, Req), State}
57 end.
58
59 state_to_details(#{username := User, password := Pass}) ->
60 52 {basic, User, Pass};
61 state_to_details(_) ->
62 47 not_configured.
63
64 check_auth({basic, _User, _Pass}, _ConfigDetails = not_configured) ->
65 5 {error, auth_header_passed_but_not_expected};
66 check_auth(_HeaderDetails, _ConfigDetails = not_configured) ->
67 42 ok;
68 check_auth({basic, User, Pass}, {basic, User, Pass}) ->
69 42 ok;
70 check_auth({basic, _, _}, {basic, _, _}) ->
71 5 {error, auth_password_invalid};
72 check_auth(_, {basic, _, _}) ->
73 5 {error, no_basic_auth_provided}.
74
75 %% Custom cowboy_rest callbacks:
76 -spec to_json(Req, State) -> {Body, Req, State} | {stop, Req, State}
77 when Req :: cowboy_req:req(), State :: state(), Body :: binary().
78 to_json(Req, State) ->
79 4 ExtDomain = cowboy_req:binding(domain, Req),
80 4 Domain = jid:nameprep(ExtDomain),
81 4 case mongoose_domain_sql:select_domain(Domain) of
82 {ok, Props} ->
83 2 {jiffy:encode(Props), Req, State};
84 {error, not_found} ->
85 2 {stop, reply_error(404, <<"domain not found">>, Req), State}
86 end.
87
88 -spec handle_domain(Req, State) -> {boolean(), Req, State}
89 when Req :: cowboy_req:req(), State :: state().
90 handle_domain(Req, State) ->
91 56 Method = cowboy_req:method(Req),
92 56 ExtDomain = cowboy_req:binding(domain, Req),
93 56 Domain = jid:nameprep(ExtDomain),
94 56 {ok, Body, Req2} = cowboy_req:read_body(Req),
95 56 MaybeParams = json_decode(Body),
96 56 case Method of
97 <<"PUT">> ->
98 34 insert_domain(Domain, MaybeParams, Req2, State);
99 <<"PATCH">> ->
100 22 patch_domain(Domain, MaybeParams, Req2, State)
101 end.
102
103 %% Private helper functions:
104 insert_domain(Domain, {ok, #{<<"host_type">> := HostType}}, Req, State) ->
105 28 case mongoose_domain_api:insert_domain(Domain, HostType) of
106 ok ->
107 18 {true, Req, State};
108 {error, duplicate} ->
109 2 {false, reply_error(409, <<"duplicate">>, Req), State};
110 {error, static} ->
111 2 {false, reply_error(403, <<"domain is static">>, Req), State};
112 {error, {db_error, _}} ->
113 2 {false, reply_error(500, <<"database error">>, Req), State};
114 {error, service_disabled} ->
115 2 {false, reply_error(403, <<"service disabled">>, Req), State};
116 {error, unknown_host_type} ->
117 2 {false, reply_error(403, <<"unknown host type">>, Req), State}
118 end;
119 insert_domain(_Domain, {ok, #{}}, Req, State) ->
120 2 {false, reply_error(400, <<"'host_type' field is missing">>, Req), State};
121 insert_domain(_Domain, {error, empty}, Req, State) ->
122 2 {false, reply_error(400, <<"body is empty">>, Req), State};
123 insert_domain(_Domain, {error, _}, Req, State) ->
124 2 {false, reply_error(400, <<"failed to parse JSON">>, Req), State}.
125
126 patch_domain(Domain, {ok, #{<<"enabled">> := true}}, Req, State) ->
127 10 Res = mongoose_domain_api:enable_domain(Domain),
128 10 handle_enabled_result(Res, Req, State);
129 patch_domain(Domain, {ok, #{<<"enabled">> := false}}, Req, State) ->
130 6 Res = mongoose_domain_api:disable_domain(Domain),
131 6 handle_enabled_result(Res, Req, State);
132 patch_domain(_Domain, {ok, #{}}, Req, State) ->
133 2 {false, reply_error(400, <<"'enabled' field is missing">>, Req), State};
134 patch_domain(_Domain, {error, empty}, Req, State) ->
135 2 {false, reply_error(400, <<"body is empty">>, Req), State};
136 patch_domain(_Domain, {error, _}, Req, State) ->
137 2 {false, reply_error(400, <<"failed to parse JSON">>, Req), State}.
138
139 handle_enabled_result(Res, Req, State) ->
140 16 case Res of
141 ok ->
142 6 {true, Req, State};
143 {error, not_found} ->
144 4 {false, reply_error(404, <<"domain not found">>, Req), State};
145 {error, static} ->
146 2 {false, reply_error(403, <<"domain is static">>, Req), State};
147 {error, service_disabled} ->
148 2 {false, reply_error(403, <<"service disabled">>, Req), State};
149 {error, {db_error, _}} ->
150 2 {false, reply_error(500, <<"database error">>, Req), State}
151 end.
152
153 delete_resource(Req, State) ->
154 24 ExtDomain = cowboy_req:binding(domain, Req),
155 24 Domain = jid:nameprep(ExtDomain),
156 24 {ok, Body, Req2} = cowboy_req:read_body(Req),
157 24 MaybeParams = json_decode(Body),
158 24 delete_domain(Domain, MaybeParams, Req2, State).
159
160 delete_domain(Domain, {ok, #{<<"host_type">> := HostType}}, Req, State) ->
161 18 case mongoose_domain_api:delete_domain(Domain, HostType) of
162 ok ->
163 8 {true, Req, State};
164 {error, {db_error, _}} ->
165 2 {false, reply_error(500, <<"database error">>, Req), State};
166 {error, static} ->
167 2 {false, reply_error(403, <<"domain is static">>, Req), State};
168 {error, service_disabled} ->
169 2 {false, reply_error(403, <<"service disabled">>, Req), State};
170 {error, wrong_host_type} ->
171 2 {false, reply_error(403, <<"wrong host type">>, Req), State};
172 {error, unknown_host_type} ->
173 2 {false, reply_error(403, <<"unknown host type">>, Req), State}
174 end;
175 delete_domain(_Domain, {ok, #{}}, Req, State) ->
176 2 {false, reply_error(400, <<"'host_type' field is missing">>, Req), State};
177 delete_domain(_Domain, {error, empty}, Req, State) ->
178 2 {false, reply_error(400, <<"body is empty">>, Req), State};
179 delete_domain(_Domain, {error, _}, Req, State) ->
180 2 {false, reply_error(400, <<"failed to parse JSON">>, Req), State}.
181
182 reply_error(Code, What, Req) ->
183 65 ?LOG_ERROR(#{what => rest_domain_failed, reason => What,
184
:-(
code => Code, req => Req}),
185 65 Body = jiffy:encode(#{what => What}),
186 65 cowboy_req:reply(Code, #{<<"content-type">> => <<"application/json">>}, Body, Req).
187
188 json_decode(<<>>) ->
189 6 {error, empty};
190 json_decode(Bin) ->
191 74 try
192 74 {ok, jiffy:decode(Bin, [return_maps])}
193 catch
194 Class:Reason ->
195 6 {error, {Class, Reason}}
196 end.
Line Hits Source