./ct_report/coverage/ejabberd_cowboy.COVER.html

1 %%%===================================================================
2 %%% @doc Common listener/router for modules that use Cowboy.
3 %%%
4 %%% The `modules' configuration option should be a list of
5 %%% {Host, BasePath, Module} or {Host, BasePath, Module, Opts} tuples,
6 %%% where a Host of "_" will match any host.
7 %%%
8 %%% A `middlewares' configuration option may be specified to configure
9 %%% Cowboy middlewares.
10 %%%
11 %%% Modules may export the following function to configure Cowboy
12 %%% routing for sub-paths:
13 %%% cowboy_router_paths(BasePath, Opts) ->
14 %%% [{PathMatch, Handler, NewOpts}]
15 %%% If not implemented, [{BasePath, Module, []|Opts}] is assumed.
16 %%% @end
17 %%%===================================================================
18 -module(ejabberd_cowboy).
19 -behavior(gen_server).
20 -behavior(cowboy_middleware).
21
22 %% ejabberd_listener API
23 -export([socket_type/0,
24 start_listener/2]).
25
26 %% cowboy_middleware API
27 -export([execute/2]).
28
29 %% gen_server API
30 -export([start_link/1]).
31 -export([init/1,
32 handle_call/3,
33 handle_cast/2,
34 handle_info/2,
35 code_change/3,
36 terminate/2]).
37
38 %% helper for internal use
39 -export([ref/1, reload_dispatch/1]).
40 -export([start_cowboy/2, stop_cowboy/1]).
41
42 -ignore_xref([behaviour_info/1, process/1, ref/1, socket_type/0, start_cowboy/2,
43 start_link/1, start_listener/2, start_listener/1, stop_cowboy/1]).
44
45 -include("mongoose.hrl").
46 -type options() :: [any()].
47 -type path() :: binary().
48 -type paths() :: [path()].
49 -type route() :: {path() | paths(), module(), options()}.
50 -type implemented_result() :: [route()].
51
52 -export_type([options/0]).
53 -export_type([path/0]).
54 -export_type([route/0]).
55 -export_type([implemented_result/0]).
56
57 -callback cowboy_router_paths(path(), options()) -> implemented_result().
58
59 -record(cowboy_state, {ref, opts = []}).
60
61 %%--------------------------------------------------------------------
62 %% ejabberd_listener API
63 %%--------------------------------------------------------------------
64
65 socket_type() ->
66 578 independent.
67
68 start_listener({Port, IP, tcp}=Listener, Opts) ->
69 578 Ref = ref(Listener),
70 578 ChildSpec = {Listener, {?MODULE, start_link,
71 [#cowboy_state{ref = Ref, opts = Opts}]},
72 transient, infinity, worker, [?MODULE]},
73 578 {ok, Pid} = supervisor:start_child(ejabberd_listeners, ChildSpec),
74 578 TransportOpts = proplists:get_value(transport_options, Opts, []),
75 578 TransportOptsMap = maps:from_list(TransportOpts),
76 578 TransportOptsMap2 = TransportOptsMap#{socket_opts => [{port, Port}, {ip, IP}]},
77 578 TransportOptsMap3 = maybe_insert_max_connections(TransportOptsMap2, Opts),
78 578 Opts2 = lists:keystore(transport_options, 1, Opts, {transport_options, TransportOptsMap3}),
79 578 {ok, _} = start_cowboy(Ref, Opts2),
80 578 {ok, Pid}.
81
82 reload_dispatch(Ref) ->
83 266 gen_server:call(Ref, reload_dispatch).
84
85 %% @doc gen_server for handling shutdown when started via ejabberd_listener
86 -spec start_link(_) -> 'ignore' | {'error', _} | {'ok', pid()}.
87 start_link(State) ->
88 578 gen_server:start_link(?MODULE, State, []).
89
90 init(State) ->
91 578 process_flag(trap_exit, true),
92 578 {ok, State}.
93
94 handle_call(reload_dispatch, _From, #cowboy_state{ref = Ref, opts = Opts} = State) ->
95 266 reload_dispatch(Ref, Opts),
96 266 {reply, ok, State};
97 handle_call(_Request, _From, State) ->
98
:-(
{noreply, State}.
99
100 handle_cast(_Request, State) ->
101
:-(
{noreply, State}.
102
103 handle_info(_Info, State) ->
104
:-(
{noreply, State}.
105
106 code_change(_OldVsn, State, _Extra) ->
107
:-(
{ok, State}.
108
109 terminate(_Reason, State) ->
110 578 stop_cowboy(State#cowboy_state.ref).
111
112 -spec handler({integer(), inet:ip_address(), tcp}) -> list().
113 handler({Port, IP, tcp}) ->
114 578 [inet_parse:ntoa(IP), <<"_">>, integer_to_list(Port)].
115
116 %%--------------------------------------------------------------------
117 %% cowboy_middleware callback
118 %%--------------------------------------------------------------------
119
120 -spec execute(cowboy_req:req(), cowboy_middleware:env()) ->
121 {ok, cowboy_req:req(), cowboy_middleware:env()}.
122 execute(Req, Env) ->
123 3290 case mongoose_config:lookup_opt(cowboy_server_name) of
124 {error, not_found} ->
125 3121 {ok, Req, Env};
126 {ok, ServerName} ->
127 169 {ok, cowboy_req:set_resp_header(<<"server">>, ServerName, Req), Env}
128 end.
129
130 %%--------------------------------------------------------------------
131 %% Internal Functions
132 %%--------------------------------------------------------------------
133
134 start_cowboy(Ref, Opts) ->
135 578 {Retries, SleepTime} = gen_mod:get_opt(retries, Opts, {20, 50}),
136 578 do_start_cowboy(Ref, Opts, Retries, SleepTime).
137
138
139 do_start_cowboy(Ref, Opts, 0, _) ->
140
:-(
do_start_cowboy(Ref, Opts);
141 do_start_cowboy(Ref, Opts, Retries, SleepTime) ->
142 578 case do_start_cowboy(Ref, Opts) of
143 {error, eaddrinuse} ->
144
:-(
timer:sleep(SleepTime),
145
:-(
do_start_cowboy(Ref, Opts, Retries - 1, SleepTime);
146 Other ->
147 578 Other
148 end.
149
150 do_start_cowboy(Ref, Opts) ->
151 578 SSLOpts = gen_mod:get_opt(ssl, Opts, undefined),
152 578 NumAcceptors = gen_mod:get_opt(num_acceptors, Opts, 100),
153 578 TransportOpts0 = gen_mod:get_opt(transport_options, Opts, #{}),
154 578 TransportOpts = TransportOpts0#{num_acceptors => NumAcceptors},
155 578 Modules = gen_mod:get_opt(modules, Opts),
156 578 Dispatch = cowboy_router:compile(get_routes(Modules)),
157 578 ProtocolOpts = [{env, [{dispatch, Dispatch}]} |
158 gen_mod:get_opt(protocol_options, Opts, [])],
159 578 ok = trails_store(Modules),
160 578 case catch start_http_or_https(SSLOpts, Ref, TransportOpts, ProtocolOpts) of
161 {error, {{shutdown,
162 {failed_to_start_child, ranch_acceptors_sup,
163 {{badmatch, {error, eaddrinuse}}, _ }}}, _}} ->
164
:-(
{error, eaddrinuse};
165 Result ->
166 578 Result
167 end.
168
169 start_http_or_https(undefined, Ref, TransportOpts, ProtocolOpts) ->
170 414 cowboy_start_http(Ref, TransportOpts, ProtocolOpts);
171 start_http_or_https(SSLOpts, Ref, TransportOpts, ProtocolOpts) ->
172 164 SSLOptsWithVerifyFun = maybe_set_verify_fun(SSLOpts),
173 164 FilteredSSLOptions = filter_options(ignored_ssl_options(), SSLOptsWithVerifyFun),
174 164 SocketOptsWithSSL = maps:get(socket_opts, TransportOpts) ++ FilteredSSLOptions,
175 164 cowboy_start_https(Ref, TransportOpts#{socket_opts => SocketOptsWithSSL}, ProtocolOpts).
176
177 cowboy_start_http(Ref, TransportOpts, ProtocolOpts) ->
178 414 ProtoOpts = add_common_middleware(make_env_map(maps:from_list(ProtocolOpts))),
179 414 cowboy:start_clear(Ref, TransportOpts, ProtoOpts).
180
181 cowboy_start_https(Ref, TransportOpts, ProtocolOpts) ->
182 164 ProtoOpts = add_common_middleware(make_env_map(maps:from_list(ProtocolOpts))),
183 164 cowboy:start_tls(Ref, TransportOpts, ProtoOpts).
184
185 make_env_map(Map = #{ env := Env }) ->
186 578 Map#{ env => maps:from_list(Env) }.
187
188 % We need to insert our middleware just before `cowboy_handler`,
189 % so the injected response header is taken into account.
190 add_common_middleware(Map = #{ middlewares := Middlewares }) ->
191
:-(
{Ms1, Ms2} = lists:splitwith(fun(Middleware) -> Middleware /= cowboy_handler end, Middlewares),
192
:-(
Map#{ middlewares := Ms1 ++ [?MODULE | Ms2] };
193 add_common_middleware(Map) ->
194 578 Map#{ middlewares => [cowboy_router, ?MODULE, cowboy_handler] }.
195
196 reload_dispatch(Ref, Opts) ->
197 266 Dispatch = cowboy_router:compile(get_routes(gen_mod:get_opt(modules, Opts))),
198 266 cowboy:set_env(Ref, dispatch, Dispatch).
199
200 stop_cowboy(Ref) ->
201 578 cowboy:stop_listener(Ref).
202
203
204 ref(Listener) ->
205 578 Ref = handler(Listener),
206 578 ModRef = [?MODULE_STRING, <<"_">>, Ref],
207 578 list_to_atom(binary_to_list(iolist_to_binary(ModRef))).
208
209 %% @doc Cowboy will search for a matching Host, then for a matching Path. If no
210 %% Path matches, Cowboy will not search for another matching Host. So, we must
211 %% merge all Paths for each Host, add any wildcard Paths to each Host, and
212 %% ensure that the wildcard Host is listed last. A dict would be slightly
213 %% easier to use here, but a proplist ensures that the user can influence Host
214 %% ordering if other wildcards like "[...]" are used.
215 get_routes(Modules) ->
216 844 Routes = get_routes(Modules, []),
217 844 WildcardPaths = proplists:get_value('_', Routes, []),
218 844 Merge = fun(Paths) -> Paths ++ WildcardPaths end,
219 844 Merged = lists:keymap(Merge, 2, proplists:delete('_', Routes)),
220 844 Final = Merged ++ [{'_', WildcardPaths}],
221 844 ?LOG_DEBUG(#{what => configured_cowboy_routes, routes => Final}),
222 844 Final.
223
224 get_routes([], Routes) ->
225 844 Routes;
226 get_routes([{Host, BasePath, Module} | Tail], Routes) ->
227
:-(
get_routes([{Host, BasePath, Module, []} | Tail], Routes);
228 get_routes([{Host, BasePath, Module, Opts} | Tail], Routes) ->
229 %% "_" is used in TOML and translated to '_' here.
230 2288 CowboyHost = case Host of
231 1800 "_" -> '_';
232 488 _ -> Host
233 end,
234 2288 ensure_loaded_module(Module),
235 2288 Paths = proplists:get_value(CowboyHost, Routes, []) ++
236 case erlang:function_exported(Module, cowboy_router_paths, 2) of
237 368 true -> Module:cowboy_router_paths(BasePath, Opts);
238 1920 _ -> [{BasePath, Module, Opts}]
239 end,
240 2288 get_routes(Tail, lists:keystore(CowboyHost, 1, Routes, {CowboyHost, Paths})).
241
242 ensure_loaded_module(Module) ->
243 2288 case code:ensure_loaded(Module) of
244 {module, Module} ->
245 2288 ok;
246 Other ->
247
:-(
erlang:error(#{issue => ensure_loaded_module_failed,
248 modue => Module,
249 reason => Other})
250 end.
251
252 ignored_ssl_options() ->
253 %% these options should be ignored if they creep into ssl section
254 164 [port, ip, max_connections, verify_mode].
255
256 filter_options(IgnoreOpts, [Opt | Opts]) when is_atom(Opt) ->
257
:-(
case lists:member(Opt, IgnoreOpts) of
258
:-(
true -> filter_options(IgnoreOpts, Opts);
259
:-(
false -> [Opt | filter_options(IgnoreOpts, Opts)]
260 end;
261 filter_options(IgnoreOpts, [Opt | Opts]) when tuple_size(Opt) >= 1 ->
262 636 case lists:member(element(1, Opt), IgnoreOpts) of
263 36 true -> filter_options(IgnoreOpts, Opts);
264 600 false -> [Opt | filter_options(IgnoreOpts, Opts)]
265 end;
266 filter_options(_, []) ->
267 164 [].
268
269 maybe_set_verify_fun(SSLOptions) ->
270 164 case proplists:get_value(verify_mode, SSLOptions, undefined) of
271 undefined ->
272 128 SSLOptions;
273 Mode ->
274 36 Fun = just_tls:verify_fun(Mode),
275 36 lists:keystore(verify_fun, 1, SSLOptions, {verify_fun, Fun})
276 end.
277
278 % This functions is for backward compatibility, as previous default config
279 % used max_connections tuple for all ejabberd_cowboy listeners
280 maybe_insert_max_connections(TransportOpts, Opts) ->
281 578 Key = max_connections,
282 578 case gen_mod:get_opt(Key, Opts, undefined) of
283 undefined ->
284 578 TransportOpts;
285 Value ->
286
:-(
TransportOpts#{Key => Value}
287 end.
288
289 %% -------------------------------------------------------------------
290 %% @private
291 %% @doc
292 %% Store trails, this is needed to generate swagger documentation.
293 %% Add to Trails each of modules where the trails behaviour is used.
294 %% The modules must be added into `mongooseim.toml' in the `swagger' section.
295 %% @end
296 %% -------------------------------------------------------------------
297 trails_store(Modules) ->
298 578 try
299 578 trails:store(trails:trails(collect_trails(Modules, [])))
300 catch Class:Reason ->
301
:-(
?LOG_WARNING(#{what => caught_exception, class => Class, reason => Reason})
302 end.
303
304 %% -------------------------------------------------------------------
305 %% @private
306 %% @doc
307 %% Helper of store trails for collect trails modules
308 %% @end
309 %% -------------------------------------------------------------------
310 collect_trails([], Acc) ->
311 578 Acc;
312 collect_trails([{Host, BasePath, Module} | T], Acc) ->
313
:-(
collect_trails([{Host, BasePath, Module, []} | T], Acc);
314 collect_trails([{_, _, Module, _} | T], Acc) ->
315 1566 case erlang:function_exported(Module, trails, 0) of
316 true ->
317 492 collect_trails(T, [Module | Acc]);
318 _ ->
319 1074 collect_trails(T, Acc)
320 end.
Line Hits Source