1 |
|
-module(mongoose_client_api). |
2 |
|
|
3 |
|
-behaviour(mongoose_http_handler). |
4 |
|
|
5 |
|
%% mongoose_http_handler callbacks |
6 |
|
-export([config_spec/0, routes/1]). |
7 |
|
|
8 |
|
%% Utilities for the handler modules |
9 |
|
-export([init/2, |
10 |
|
is_authorized/2, |
11 |
|
parse_body/1, |
12 |
|
parse_qs/1, |
13 |
|
try_handle_request/3, |
14 |
|
throw_error/2]). |
15 |
|
|
16 |
|
-include("mongoose.hrl"). |
17 |
|
-include("mongoose_config_spec.hrl"). |
18 |
|
|
19 |
|
-type handler_options() :: #{path := string(), handlers := [module()], docs := boolean(), |
20 |
|
atom() => any()}. |
21 |
|
-type req() :: cowboy_req:req(). |
22 |
|
-type state() :: #{atom() => any()}. |
23 |
|
-type error_type() :: bad_request | denied | not_found. |
24 |
|
|
25 |
|
-callback routes() -> mongoose_http_handler:routes(). |
26 |
|
|
27 |
|
%% mongoose_http_handler callbacks |
28 |
|
|
29 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
30 |
|
config_spec() -> |
31 |
104 |
Handlers = all_handlers(), |
32 |
104 |
#section{items = #{<<"handlers">> => #list{items = #option{type = atom, |
33 |
|
validate = {enum, Handlers}}, |
34 |
|
validate = unique}, |
35 |
|
<<"docs">> => #option{type = boolean}}, |
36 |
|
defaults = #{<<"handlers">> => Handlers, |
37 |
|
<<"docs">> => true}}. |
38 |
|
|
39 |
|
-spec routes(handler_options()) -> mongoose_http_handler:routes(). |
40 |
|
routes(Opts = #{path := BasePath}) -> |
41 |
756 |
[{[BasePath, Path], Module, ModuleOpts} |
42 |
108 |
|| {Path, Module, ModuleOpts} <- api_paths(Opts)] ++ api_doc_paths(Opts). |
43 |
|
|
44 |
|
all_handlers() -> |
45 |
104 |
[sse, messages, contacts, rooms, rooms_config, rooms_users, rooms_messages]. |
46 |
|
|
47 |
|
-spec api_paths(handler_options()) -> mongoose_http_handler:routes(). |
48 |
|
api_paths(#{handlers := Handlers}) -> |
49 |
108 |
lists:flatmap(fun api_paths_for_handler/1, Handlers). |
50 |
|
|
51 |
|
api_paths_for_handler(Handler) -> |
52 |
756 |
HandlerModule = list_to_existing_atom("mongoose_client_api_" ++ atom_to_list(Handler)), |
53 |
756 |
HandlerModule:routes(). |
54 |
|
|
55 |
|
api_doc_paths(#{docs := true}) -> |
56 |
108 |
[{"/api-docs", cowboy_swagger_redirect_handler, #{}}, |
57 |
|
{"/api-docs/swagger.json", cowboy_swagger_json_handler, #{}}, |
58 |
|
{"/api-docs/[...]", cowboy_static, {priv_dir, cowboy_swagger, "swagger", |
59 |
|
[{mimetypes, cow_mimetypes, all}]} |
60 |
|
}]; |
61 |
|
api_doc_paths(#{docs := false}) -> |
62 |
:-( |
[]. |
63 |
|
|
64 |
|
init(Req, _Opts) -> |
65 |
180 |
State = #{}, |
66 |
180 |
case cowboy_req:header(<<"origin">>, Req) of |
67 |
|
undefined -> |
68 |
180 |
{cowboy_rest, Req, State}; |
69 |
|
Origin -> |
70 |
:-( |
Req1 = set_cors_headers(Origin, Req), |
71 |
:-( |
{cowboy_rest, Req1, State} |
72 |
|
end. |
73 |
|
|
74 |
|
set_cors_headers(Origin, Req) -> |
75 |
|
%% set CORS headers |
76 |
:-( |
Headers = [{<<"access-control-allow-origin">>, Origin}, |
77 |
|
{<<"access-control-allow-methods">>, <<"GET, OPTIONS">>}, |
78 |
|
{<<"access-control-allow-credentials">>, <<"true">>}, |
79 |
|
{<<"access-control-allow-headers">>, <<"authorization, content-type">>} |
80 |
|
], |
81 |
|
|
82 |
:-( |
lists:foldl(fun set_cors_header/2, Req, Headers). |
83 |
|
|
84 |
|
set_cors_header({Header, Value}, Req) -> |
85 |
:-( |
cowboy_req:set_resp_header(Header, Value, Req). |
86 |
|
|
87 |
|
%%-------------------------------------------------------------------- |
88 |
|
%% Authorization |
89 |
|
%%-------------------------------------------------------------------- |
90 |
|
|
91 |
|
% @doc cowboy callback |
92 |
|
is_authorized(Req, State) -> |
93 |
180 |
HTTPMethod = cowboy_req:method(Req), |
94 |
180 |
AuthDetails = mongoose_api_common:get_auth_details(Req), |
95 |
180 |
case AuthDetails of |
96 |
|
undefined -> |
97 |
2 |
mongoose_api_common:make_unauthorized_response(Req, State); |
98 |
|
{AuthMethod, User, Password} -> |
99 |
178 |
authorize(AuthMethod, User, Password, HTTPMethod, Req, State) |
100 |
|
end. |
101 |
|
|
102 |
|
authorize(AuthMethod, User, Password, HTTPMethod, Req, State) -> |
103 |
178 |
MaybeJID = jid:from_binary(User), |
104 |
178 |
case do_authorize(AuthMethod, MaybeJID, Password, HTTPMethod) of |
105 |
|
noauth -> |
106 |
:-( |
{true, Req, State}; |
107 |
|
{true, Creds} -> |
108 |
178 |
{true, Req, State#{user => User, jid => MaybeJID, creds => Creds}}; |
109 |
|
false -> |
110 |
:-( |
mongoose_api_common:make_unauthorized_response(Req, State) |
111 |
|
end. |
112 |
|
|
113 |
|
do_authorize(AuthMethod, MaybeJID, Password, HTTPMethod) -> |
114 |
178 |
case is_noauth_http_method(HTTPMethod) of |
115 |
|
true -> |
116 |
:-( |
noauth; |
117 |
|
false -> |
118 |
178 |
mongoose_api_common:is_known_auth_method(AuthMethod) andalso |
119 |
178 |
mongoose_api_common:check_password(MaybeJID, Password) |
120 |
|
end. |
121 |
|
|
122 |
|
% Constraints |
123 |
:-( |
is_noauth_http_method(<<"OPTIONS">>) -> true; |
124 |
178 |
is_noauth_http_method(_) -> false. |
125 |
|
|
126 |
|
-spec parse_body(req()) -> #{atom() => jiffy:json_value()}. |
127 |
|
parse_body(Req) -> |
128 |
102 |
try |
129 |
102 |
{ok, Body, _Req2} = cowboy_req:read_body(Req), |
130 |
102 |
decoded_json_to_map(jiffy:decode(Body)) |
131 |
|
catch Class:Reason:Stacktrace -> |
132 |
1 |
?LOG_INFO(#{what => parse_body_failed, |
133 |
1 |
class => Class, reason => Reason, stacktrace => Stacktrace}), |
134 |
1 |
throw_error(bad_request, <<"Invalid request body">>) |
135 |
|
end. |
136 |
|
|
137 |
|
decoded_json_to_map({L}) when is_list(L) -> |
138 |
106 |
maps:from_list([{binary_to_existing_atom(K), decoded_json_to_map(V)} || {K, V} <- L]); |
139 |
|
decoded_json_to_map(V) -> |
140 |
150 |
V. |
141 |
|
|
142 |
|
-spec parse_qs(req()) -> #{atom() => binary() | true}. |
143 |
|
parse_qs(Req) -> |
144 |
27 |
try |
145 |
27 |
maps:from_list([{binary_to_existing_atom(K), V} || {K, V} <- cowboy_req:parse_qs(Req)]) |
146 |
|
catch Class:Reason:Stacktrace -> |
147 |
2 |
?LOG_INFO(#{what => parse_qs_failed, |
148 |
2 |
class => Class, reason => Reason, stacktrace => Stacktrace}), |
149 |
2 |
throw_error(bad_request, <<"Invalid query string">>) |
150 |
|
end. |
151 |
|
|
152 |
|
-spec try_handle_request(req(), state(), fun((req(), state()) -> Result)) -> Result. |
153 |
|
try_handle_request(Req, State, F) -> |
154 |
174 |
try |
155 |
174 |
F(Req, State) |
156 |
|
catch throw:#{error_type := ErrorType, message := Msg} -> |
157 |
45 |
error_response(ErrorType, Msg, Req, State) |
158 |
|
end. |
159 |
|
|
160 |
|
-spec throw_error(error_type(), iodata()) -> no_return(). |
161 |
|
throw_error(ErrorType, Msg) -> |
162 |
45 |
throw(#{error_type => ErrorType, message => Msg}). |
163 |
|
|
164 |
|
-spec error_response(error_type(), iodata(), req(), state()) -> {stop, req(), state()}. |
165 |
|
error_response(ErrorType, Message, Req, State) -> |
166 |
45 |
BinMessage = iolist_to_binary(Message), |
167 |
45 |
?LOG(log_level(ErrorType), #{what => mongoose_client_api_error_response, |
168 |
|
error_type => ErrorType, |
169 |
|
message => BinMessage, |
170 |
45 |
req => Req}), |
171 |
45 |
Req1 = cowboy_req:reply(error_code(ErrorType), #{}, jiffy:encode(BinMessage), Req), |
172 |
45 |
{stop, Req1, State}. |
173 |
|
|
174 |
|
-spec error_code(error_type()) -> non_neg_integer(). |
175 |
27 |
error_code(bad_request) -> 400; |
176 |
8 |
error_code(denied) -> 403; |
177 |
10 |
error_code(not_found) -> 404. |
178 |
|
|
179 |
|
-spec log_level(error_type()) -> logger:level(). |
180 |
27 |
log_level(bad_request) -> info; |
181 |
8 |
log_level(denied) -> info; |
182 |
10 |
log_level(not_found) -> info. |