./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 33 Handlers = all_handlers(),
32 33 #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 259 [{[BasePath, Path], Module, ModuleOpts}
42 37 || {Path, Module, ModuleOpts} <- api_paths(Opts)] ++ api_doc_paths(Opts).
43
44 all_handlers() ->
45 33 [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 37 lists:flatmap(fun api_paths_for_handler/1, Handlers).
50
51 api_paths_for_handler(Handler) ->
52 259 HandlerModule = list_to_existing_atom("mongoose_client_api_" ++ atom_to_list(Handler)),
53 259 HandlerModule:routes().
54
55 api_doc_paths(#{docs := true}) ->
56 37 [{"/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.
Line Hits Source