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