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 |
|
-export([init/2]). |
9 |
|
-export([content_types_provided/2]). |
10 |
|
-export([is_authorized/2]). |
11 |
|
-export([options/2]). |
12 |
|
-export([allowed_methods/2]). |
13 |
|
-export([to_json/2]). |
14 |
|
-export([bad_request/2]). |
15 |
|
-export([bad_request/3]). |
16 |
|
-export([forbidden_request/2]). |
17 |
|
-export([forbidden_request/3]). |
18 |
|
-export([json_to_map/1]). |
19 |
|
|
20 |
|
-ignore_xref([allowed_methods/2, content_types_provided/2, forbidden_request/3, |
21 |
|
options/2, to_json/2]). |
22 |
|
|
23 |
|
-include("mongoose.hrl"). |
24 |
|
-include("mongoose_config_spec.hrl"). |
25 |
|
|
26 |
|
-type handler_options() :: #{path := string(), handlers := [module()], docs := boolean(), |
27 |
|
atom() => any()}. |
28 |
|
|
29 |
|
%% mongoose_http_handler callbacks |
30 |
|
|
31 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
32 |
|
config_spec() -> |
33 |
83 |
HandlerModules = [Module || {_, Module, _} <- api_paths()], |
34 |
83 |
#section{items = #{<<"handlers">> => #list{items = #option{type = atom, |
35 |
|
validate = {enum, HandlerModules}}, |
36 |
|
validate = unique}, |
37 |
|
<<"docs">> => #option{type = boolean}}, |
38 |
|
defaults = #{<<"handlers">> => HandlerModules, |
39 |
|
<<"docs">> => true}}. |
40 |
|
|
41 |
|
-spec routes(handler_options()) -> mongoose_http_handler:routes(). |
42 |
|
routes(Opts = #{path := BasePath}) -> |
43 |
1085 |
[{[BasePath, Path], Module, ModuleOpts} |
44 |
155 |
|| {Path, Module, ModuleOpts} <- api_paths(Opts)] ++ api_doc_paths(Opts). |
45 |
|
|
46 |
|
api_paths(#{handlers := HandlerModules}) -> |
47 |
155 |
lists:filter(fun({_, Module, _}) -> lists:member(Module, HandlerModules) end, api_paths()). |
48 |
|
|
49 |
|
api_paths() -> |
50 |
238 |
[{"/sse", lasse_handler, #{module => mongoose_client_api_sse}}, |
51 |
|
{"/messages/[:with]", mongoose_client_api_messages, #{}}, |
52 |
|
{"/contacts/[:jid]", mongoose_client_api_contacts, #{}}, |
53 |
|
{"/rooms/[:id]", mongoose_client_api_rooms, #{}}, |
54 |
|
{"/rooms/[:id]/config", mongoose_client_api_rooms_config, #{}}, |
55 |
|
{"/rooms/:id/users/[:user]", mongoose_client_api_rooms_users, #{}}, |
56 |
|
{"/rooms/[:id]/messages", mongoose_client_api_rooms_messages, #{}}]. |
57 |
|
|
58 |
|
api_doc_paths(#{docs := true}) -> |
59 |
155 |
[{"/api-docs", cowboy_swagger_redirect_handler, #{}}, |
60 |
|
{"/api-docs/swagger.json", cowboy_swagger_json_handler, #{}}, |
61 |
|
{"/api-docs/[...]", cowboy_static, {priv_dir, cowboy_swagger, "swagger", |
62 |
|
[{mimetypes, cow_mimetypes, all}]} |
63 |
|
}]; |
64 |
|
api_doc_paths(#{docs := false}) -> |
65 |
:-( |
[]. |
66 |
|
|
67 |
|
init(Req, _Opts) -> |
68 |
99 |
State = #{}, |
69 |
99 |
case cowboy_req:header(<<"origin">>, Req) of |
70 |
|
undefined -> |
71 |
99 |
{cowboy_rest, Req, State}; |
72 |
|
Origin -> |
73 |
:-( |
Req1 = set_cors_headers(Origin, Req), |
74 |
:-( |
{cowboy_rest, Req1, State} |
75 |
|
end. |
76 |
|
|
77 |
|
set_cors_headers(Origin, Req) -> |
78 |
|
%% set CORS headers |
79 |
:-( |
Headers = [{<<"access-control-allow-origin">>, Origin}, |
80 |
|
{<<"access-control-allow-methods">>, <<"GET, OPTIONS">>}, |
81 |
|
{<<"access-control-allow-credentials">>, <<"true">>}, |
82 |
|
{<<"access-control-allow-headers">>, <<"authorization, content-type">>} |
83 |
|
], |
84 |
|
|
85 |
:-( |
lists:foldl(fun set_cors_header/2, Req, Headers). |
86 |
|
|
87 |
|
set_cors_header({Header, Value}, Req) -> |
88 |
:-( |
cowboy_req:set_resp_header(Header, Value, Req). |
89 |
|
|
90 |
|
allowed_methods(Req, State) -> |
91 |
:-( |
{[<<"OPTIONS">>, <<"GET">>], Req, State}. |
92 |
|
|
93 |
|
content_types_provided(Req, State) -> |
94 |
:-( |
{[ |
95 |
|
{{<<"application">>, <<"json">>, '*'}, to_json} |
96 |
|
], Req, State}. |
97 |
|
|
98 |
|
options(Req, State) -> |
99 |
:-( |
{ok, Req, State}. |
100 |
|
|
101 |
|
to_json(Req, User) -> |
102 |
:-( |
{<<"{}">>, Req, User}. |
103 |
|
|
104 |
|
bad_request(Req, State) -> |
105 |
3 |
bad_request(Req, <<"Bad request. The details are unknown.">>, State). |
106 |
|
|
107 |
|
bad_request(Req, Reason, State) -> |
108 |
5 |
reply(400, Req, Reason, State). |
109 |
|
|
110 |
|
forbidden_request(Req, State) -> |
111 |
3 |
forbidden_request(Req, <<>>, State). |
112 |
|
|
113 |
|
forbidden_request(Req, Reason, State) -> |
114 |
3 |
reply(403, Req, Reason, State). |
115 |
|
|
116 |
|
reply(StatusCode, Req, Body, State) -> |
117 |
8 |
maybe_report_error(StatusCode, Req, Body), |
118 |
8 |
Req1 = set_resp_body_if_missing(Body, Req), |
119 |
8 |
Req2 = cowboy_req:reply(StatusCode, Req1), |
120 |
8 |
{stop, Req2, State#{was_replied => true}}. |
121 |
|
|
122 |
|
set_resp_body_if_missing(Body, Req) -> |
123 |
8 |
case cowboy_req:has_resp_body(Req) of |
124 |
|
true -> |
125 |
3 |
Req; |
126 |
|
false -> |
127 |
5 |
cowboy_req:set_resp_body(Body, Req) |
128 |
|
end. |
129 |
|
|
130 |
|
maybe_report_error(StatusCode, Req, Body) when StatusCode >= 400 -> |
131 |
8 |
?LOG_WARNING(#{what => reply_error, |
132 |
|
stacktrace => element(2, erlang:process_info(self(), current_stacktrace)), |
133 |
:-( |
code => StatusCode, req => Req, reply_body => Body}); |
134 |
|
maybe_report_error(_StatusCode, _Req, _Body) -> |
135 |
:-( |
ok. |
136 |
|
|
137 |
|
%%-------------------------------------------------------------------- |
138 |
|
%% Authorization |
139 |
|
%%-------------------------------------------------------------------- |
140 |
|
|
141 |
|
% @doc cowboy callback |
142 |
|
is_authorized(Req, State) -> |
143 |
99 |
HTTPMethod = cowboy_req:method(Req), |
144 |
99 |
AuthDetails = mongoose_api_common:get_auth_details(Req), |
145 |
99 |
case AuthDetails of |
146 |
|
undefined -> |
147 |
2 |
mongoose_api_common:make_unauthorized_response(Req, State); |
148 |
|
{AuthMethod, User, Password} -> |
149 |
97 |
authorize(AuthMethod, User, Password, HTTPMethod, Req, State) |
150 |
|
end. |
151 |
|
|
152 |
|
authorize(AuthMethod, User, Password, HTTPMethod, Req, State) -> |
153 |
97 |
MaybeJID = jid:from_binary(User), |
154 |
97 |
case do_authorize(AuthMethod, MaybeJID, Password, HTTPMethod) of |
155 |
|
noauth -> |
156 |
:-( |
{true, Req, State}; |
157 |
|
{true, Creds} -> |
158 |
97 |
{true, Req, State#{user => User, jid => MaybeJID, creds => Creds}}; |
159 |
|
false -> |
160 |
:-( |
mongoose_api_common:make_unauthorized_response(Req, State) |
161 |
|
end. |
162 |
|
|
163 |
|
do_authorize(AuthMethod, MaybeJID, Password, HTTPMethod) -> |
164 |
97 |
case is_noauth_http_method(HTTPMethod) of |
165 |
|
true -> |
166 |
:-( |
noauth; |
167 |
|
false -> |
168 |
97 |
mongoose_api_common:is_known_auth_method(AuthMethod) andalso |
169 |
97 |
mongoose_api_common:check_password(MaybeJID, Password) |
170 |
|
end. |
171 |
|
|
172 |
|
% Constraints |
173 |
:-( |
is_noauth_http_method(<<"OPTIONS">>) -> true; |
174 |
97 |
is_noauth_http_method(_) -> false. |
175 |
|
|
176 |
|
%% ------------------------------------------------------------------- |
177 |
|
%% @doc |
178 |
|
%% Decode JSON binary into map |
179 |
|
%% @end |
180 |
|
%% ------------------------------------------------------------------- |
181 |
|
-spec json_to_map(JsonBin :: binary()) -> {ok, Map :: maps:map()} | {error, invalid_json}. |
182 |
|
|
183 |
|
json_to_map(JsonBin) -> |
184 |
60 |
case catch jiffy:decode(JsonBin, [return_maps]) of |
185 |
|
Map when is_map(Map) -> |
186 |
59 |
{ok, Map}; |
187 |
|
_ -> |
188 |
1 |
{error, invalid_json} |
189 |
|
end. |