./ct_report/coverage/mongoose_client_api.COVER.html

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 4 Handlers = all_handlers(),
32 4 #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 28 [{[BasePath, Path], Module, ModuleOpts}
42 4 || {Path, Module, ModuleOpts} <- api_paths(Opts)] ++ api_doc_paths(Opts).
43
44 all_handlers() ->
45 4 [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 4 lists:flatmap(fun api_paths_for_handler/1, Handlers).
50
51 api_paths_for_handler(Handler) ->
52 28 HandlerModule = list_to_existing_atom("mongoose_client_api_" ++ atom_to_list(Handler)),
53 28 HandlerModule:routes().
54
55 api_doc_paths(#{docs := true}) ->
56 4 [{"/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
:-(
State = #{},
66
:-(
case cowboy_req:header(<<"origin">>, Req) of
67 undefined ->
68
:-(
{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
:-(
HTTPMethod = cowboy_req:method(Req),
94
:-(
AuthDetails = mongoose_api_common:get_auth_details(Req),
95
:-(
case AuthDetails of
96 undefined ->
97
:-(
mongoose_api_common:make_unauthorized_response(Req, State);
98 {AuthMethod, User, Password} ->
99
:-(
authorize(AuthMethod, User, Password, HTTPMethod, Req, State)
100 end.
101
102 authorize(AuthMethod, User, Password, HTTPMethod, Req, State) ->
103
:-(
MaybeJID = jid:from_binary(User),
104
:-(
case do_authorize(AuthMethod, MaybeJID, Password, HTTPMethod) of
105 noauth ->
106
:-(
{true, Req, State};
107 {true, Creds} ->
108
:-(
{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
:-(
case is_noauth_http_method(HTTPMethod) of
115 true ->
116
:-(
noauth;
117 false ->
118
:-(
mongoose_api_common:is_known_auth_method(AuthMethod) andalso
119
:-(
mongoose_api_common:check_password(MaybeJID, Password)
120 end.
121
122 % Constraints
123
:-(
is_noauth_http_method(<<"OPTIONS">>) -> true;
124
:-(
is_noauth_http_method(_) -> false.
125
126 -spec parse_body(req()) -> #{atom() => jiffy:json_value()}.
127 parse_body(Req) ->
128
:-(
try
129
:-(
{ok, Body, _Req2} = cowboy_req:read_body(Req),
130
:-(
decoded_json_to_map(jiffy:decode(Body))
131 catch Class:Reason:Stacktrace ->
132
:-(
?LOG_INFO(#{what => parse_body_failed,
133
:-(
class => Class, reason => Reason, stacktrace => Stacktrace}),
134
:-(
throw_error(bad_request, <<"Invalid request body">>)
135 end.
136
137 decoded_json_to_map({L}) when is_list(L) ->
138
:-(
maps:from_list([{binary_to_existing_atom(K), decoded_json_to_map(V)} || {K, V} <- L]);
139 decoded_json_to_map(V) ->
140
:-(
V.
141
142 -spec parse_qs(req()) -> #{atom() => binary() | true}.
143 parse_qs(Req) ->
144
:-(
try
145
:-(
maps:from_list([{binary_to_existing_atom(K), V} || {K, V} <- cowboy_req:parse_qs(Req)])
146 catch Class:Reason:Stacktrace ->
147
:-(
?LOG_INFO(#{what => parse_qs_failed,
148
:-(
class => Class, reason => Reason, stacktrace => Stacktrace}),
149
:-(
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
:-(
try
155
:-(
F(Req, State)
156 catch throw:#{error_type := ErrorType, message := Msg} ->
157
:-(
error_response(ErrorType, Msg, Req, State)
158 end.
159
160 -spec throw_error(error_type(), iodata()) -> no_return().
161 throw_error(ErrorType, Msg) ->
162
:-(
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
:-(
BinMessage = iolist_to_binary(Message),
167
:-(
?LOG(log_level(ErrorType), #{what => mongoose_client_api_error_response,
168 error_type => ErrorType,
169 message => BinMessage,
170
:-(
req => Req}),
171
:-(
Req1 = cowboy_req:reply(error_code(ErrorType), #{}, jiffy:encode(BinMessage), Req),
172
:-(
{stop, Req1, State}.
173
174 -spec error_code(error_type()) -> non_neg_integer().
175
:-(
error_code(bad_request) -> 400;
176
:-(
error_code(denied) -> 403;
177
:-(
error_code(not_found) -> 404.
178
179 -spec log_level(error_type()) -> logger:level().
180
:-(
log_level(bad_request) -> info;
181
:-(
log_level(denied) -> info;
182
:-(
log_level(not_found) -> info.
Line Hits Source