./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 -behaviour(gen_server).
20 -behaviour(cowboy_middleware).
21 -behaviour(mongoose_listener).
22
23 %% mongoose_listener API
24 -export([socket_type/0,
25 start_listener/1]).
26
27 %% cowboy_middleware API
28 -export([execute/2]).
29
30 %% gen_server API
31 -export([start_link/1]).
32 -export([init/1,
33 handle_call/3,
34 handle_cast/2,
35 handle_info/2,
36 code_change/3,
37 terminate/2]).
38
39 %% helper for internal use
40 -export([ref/1, reload_dispatch/1]).
41 -export([start_cowboy/4, start_cowboy/2, stop_cowboy/1]).
42
43 -ignore_xref([behaviour_info/1, process/1, ref/1, socket_type/0, start_cowboy/2,
44 start_cowboy/4, start_link/1, start_listener/2, start_listener/1, stop_cowboy/1]).
45
46 -include("mongoose.hrl").
47 -type options() :: [any()].
48 -type path() :: binary().
49 -type paths() :: [path()].
50 -type route() :: {path() | paths(), module(), options()}.
51 -type implemented_result() :: [route()].
52
53 -type listener_options() :: #{port := inet:port_number(),
54 ip_tuple := inet:ip_address(),
55 ip_address := string(),
56 ip_version := 4 | 6,
57 proto := tcp,
58 handlers := list(),
59 transport := ranch:opts(),
60 protocol := cowboy:opts(),
61 atom() => any()}.
62
63 -export_type([options/0]).
64 -export_type([path/0]).
65 -export_type([route/0]).
66 -export_type([implemented_result/0]).
67
68 -callback cowboy_router_paths(path(), options()) -> implemented_result().
69
70 -record(cowboy_state, {ref :: atom(), opts :: listener_options()}).
71
72 %%--------------------------------------------------------------------
73 %% mongoose_listener API
74 %%--------------------------------------------------------------------
75
76 -spec socket_type() -> mongoose_listener:socket_type().
77 socket_type() ->
78
:-(
independent.
79
80 -spec start_listener(listener_options()) -> ok.
81 start_listener(Opts = #{proto := tcp}) ->
82 614 ListenerId = mongoose_listener_config:listener_id(Opts),
83 614 Ref = ref(ListenerId),
84 614 ChildSpec = #{id => ListenerId,
85 start => {?MODULE, start_link, [#cowboy_state{ref = Ref, opts = Opts}]},
86 restart => transient,
87 shutdown => infinity,
88 modules => [?MODULE]},
89 614 mongoose_listener_sup:start_child(ChildSpec),
90 614 {ok, _} = start_cowboy(Ref, Opts),
91 614 ok.
92
93 reload_dispatch(Ref) ->
94 704 gen_server:call(Ref, reload_dispatch).
95
96 %% @doc gen_server for handling shutdown when started via mongoose_listener
97 -spec start_link(_) -> 'ignore' | {'error', _} | {'ok', pid()}.
98 start_link(State) ->
99 614 gen_server:start_link(?MODULE, State, []).
100
101 init(State) ->
102 614 process_flag(trap_exit, true),
103 614 {ok, State}.
104
105 handle_call(reload_dispatch, _From, #cowboy_state{ref = Ref, opts = Opts} = State) ->
106 704 reload_dispatch(Ref, Opts),
107 704 {reply, ok, State};
108 handle_call(_Request, _From, State) ->
109
:-(
{noreply, State}.
110
111 handle_cast(_Request, State) ->
112
:-(
{noreply, State}.
113
114 handle_info(_Info, State) ->
115
:-(
{noreply, State}.
116
117 code_change(_OldVsn, State, _Extra) ->
118
:-(
{ok, State}.
119
120 terminate(_Reason, State) ->
121 614 stop_cowboy(State#cowboy_state.ref).
122
123 -spec handler({integer(), inet:ip_address(), tcp}) -> list().
124 handler({Port, IP, tcp}) ->
125 614 [inet_parse:ntoa(IP), <<"_">>, integer_to_list(Port)].
126
127 %%--------------------------------------------------------------------
128 %% cowboy_middleware callback
129 %%--------------------------------------------------------------------
130
131 -spec execute(cowboy_req:req(), cowboy_middleware:env()) ->
132 {ok, cowboy_req:req(), cowboy_middleware:env()}.
133 execute(Req, Env) ->
134 3573 case mongoose_config:lookup_opt(cowboy_server_name) of
135 {error, not_found} ->
136 3404 {ok, Req, Env};
137 {ok, ServerName} ->
138 169 {ok, cowboy_req:set_resp_header(<<"server">>, ServerName, Req), Env}
139 end.
140
141 %%--------------------------------------------------------------------
142 %% Internal Functions
143 %%--------------------------------------------------------------------
144
145 -spec start_cowboy(atom(), listener_options()) -> {ok, pid()} | {error, any()}.
146 start_cowboy(Ref, Opts) ->
147 614 start_cowboy(Ref, Opts, 20, 50).
148
149 -spec start_cowboy(atom(), listener_options(),
150 Retries :: non_neg_integer(), SleepTime :: non_neg_integer()) ->
151 {ok, pid()} | {error, any()}.
152 start_cowboy(Ref, Opts, 0, _) ->
153
:-(
do_start_cowboy(Ref, Opts);
154 start_cowboy(Ref, Opts, Retries, SleepTime) ->
155 614 case do_start_cowboy(Ref, Opts) of
156 {error, eaddrinuse} ->
157
:-(
timer:sleep(SleepTime),
158
:-(
start_cowboy(Ref, Opts, Retries - 1, SleepTime);
159 Other ->
160 614 Other
161 end.
162
163 -spec do_start_cowboy(atom(), listener_options()) -> {ok, pid()} | {error, any()}.
164 do_start_cowboy(Ref, Opts) ->
165 614 #{ip_tuple := IPTuple, port := Port, handlers := Modules,
166 transport := TransportOpts0, protocol := ProtocolOpts0} = Opts,
167 614 Dispatch = cowboy_router:compile(get_routes(Modules)),
168 614 ProtocolOpts = ProtocolOpts0#{env => #{dispatch => Dispatch}},
169 614 TransportOpts = TransportOpts0#{socket_opts => [{port, Port}, {ip, IPTuple}]},
170 614 ok = trails_store(Modules),
171 614 case catch start_http_or_https(Opts, Ref, TransportOpts, ProtocolOpts) of
172 {error, {{shutdown,
173 {failed_to_start_child, ranch_acceptors_sup,
174 {{badmatch, {error, eaddrinuse}}, _ }}}, _}} ->
175
:-(
{error, eaddrinuse};
176 Result ->
177 614 Result
178 end.
179
180 start_http_or_https(#{tls := SSLOpts}, Ref, TransportOpts, ProtocolOpts) ->
181 152 SSLOptsWithVerifyFun = maybe_set_verify_fun(SSLOpts),
182 152 SocketOptsWithSSL = maps:get(socket_opts, TransportOpts) ++ SSLOptsWithVerifyFun,
183 152 cowboy_start_https(Ref, TransportOpts#{socket_opts := SocketOptsWithSSL}, ProtocolOpts);
184 start_http_or_https(#{}, Ref, TransportOpts, ProtocolOpts) ->
185 462 cowboy_start_http(Ref, TransportOpts, ProtocolOpts).
186
187 cowboy_start_http(Ref, TransportOpts, ProtocolOpts) ->
188 462 ProtoOpts = add_common_middleware(ProtocolOpts),
189 462 cowboy:start_clear(Ref, TransportOpts, ProtoOpts).
190
191 cowboy_start_https(Ref, TransportOpts, ProtocolOpts) ->
192 152 ProtoOpts = add_common_middleware(ProtocolOpts),
193 152 cowboy:start_tls(Ref, TransportOpts, ProtoOpts).
194
195 % We need to insert our middleware just before `cowboy_handler`,
196 % so the injected response header is taken into account.
197 add_common_middleware(Map = #{ middlewares := Middlewares }) ->
198
:-(
{Ms1, Ms2} = lists:splitwith(fun(Middleware) -> Middleware /= cowboy_handler end, Middlewares),
199
:-(
Map#{ middlewares := Ms1 ++ [?MODULE | Ms2] };
200 add_common_middleware(Map) ->
201 614 Map#{ middlewares => [cowboy_router, ?MODULE, cowboy_handler] }.
202
203 reload_dispatch(Ref, #{handlers := Modules}) ->
204 704 Dispatch = cowboy_router:compile(get_routes(Modules)),
205 704 cowboy:set_env(Ref, dispatch, Dispatch).
206
207 stop_cowboy(Ref) ->
208 614 cowboy:stop_listener(Ref).
209
210
211 ref(Listener) ->
212 614 Ref = handler(Listener),
213 614 ModRef = [?MODULE_STRING, <<"_">>, Ref],
214 614 list_to_atom(binary_to_list(iolist_to_binary(ModRef))).
215
216 %% @doc Cowboy will search for a matching Host, then for a matching Path. If no
217 %% Path matches, Cowboy will not search for another matching Host. So, we must
218 %% merge all Paths for each Host, add any wildcard Paths to each Host, and
219 %% ensure that the wildcard Host is listed last. A dict would be slightly
220 %% easier to use here, but a proplist ensures that the user can influence Host
221 %% ordering if other wildcards like "[...]" are used.
222 get_routes(Modules) ->
223 1318 Routes = get_routes(Modules, []),
224 1318 WildcardPaths = proplists:get_value('_', Routes, []),
225 1318 Merge = fun(Paths) -> Paths ++ WildcardPaths end,
226 1318 Merged = lists:keymap(Merge, 2, proplists:delete('_', Routes)),
227 1318 Final = Merged ++ [{'_', WildcardPaths}],
228 1318 ?LOG_DEBUG(#{what => configured_cowboy_routes, routes => Final}),
229 1318 Final.
230
231 get_routes([], Routes) ->
232 1318 Routes;
233 get_routes([{Host, BasePath, Module} | Tail], Routes) ->
234
:-(
get_routes([{Host, BasePath, Module, []} | Tail], Routes);
235 get_routes([{Host, BasePath, Module, Opts} | Tail], Routes) ->
236 %% "_" is used in TOML and translated to '_' here.
237 3290 CowboyHost = case Host of
238 2624 "_" -> '_';
239 666 _ -> Host
240 end,
241 3290 ensure_loaded_module(Module),
242 3290 Paths = proplists:get_value(CowboyHost, Routes, []) ++
243 case erlang:function_exported(Module, cowboy_router_paths, 2) of
244 502 true -> Module:cowboy_router_paths(BasePath, Opts);
245 2788 _ -> [{BasePath, Module, Opts}]
246 end,
247 3290 get_routes(Tail, lists:keystore(CowboyHost, 1, Routes, {CowboyHost, Paths})).
248
249 ensure_loaded_module(Module) ->
250 3290 case code:ensure_loaded(Module) of
251 {module, Module} ->
252 3290 ok;
253 Other ->
254
:-(
erlang:error(#{issue => ensure_loaded_module_failed,
255 modue => Module,
256 reason => Other})
257 end.
258
259 maybe_set_verify_fun(SSLOptions) ->
260 152 case lists:keytake(verify_mode, 1, SSLOptions) of
261 false ->
262 116 SSLOptions;
263 {value, {_, Mode}, SSLOptions1} ->
264 36 Fun = just_tls:verify_fun(Mode),
265 36 [{verify_fun, Fun} | SSLOptions1]
266 end.
267
268 %% -------------------------------------------------------------------
269 %% @private
270 %% @doc
271 %% Store trails, this is needed to generate swagger documentation.
272 %% Add to Trails each of modules where the trails behaviour is used.
273 %% The modules must be added into `mongooseim.toml' in the `swagger' section.
274 %% @end
275 %% -------------------------------------------------------------------
276 trails_store(Modules) ->
277 614 try
278 614 trails:store(trails:trails(collect_trails(Modules, [])))
279 catch Class:Reason ->
280
:-(
?LOG_WARNING(#{what => caught_exception, class => Class, reason => Reason})
281 end.
282
283 %% -------------------------------------------------------------------
284 %% @private
285 %% @doc
286 %% Helper of store trails for collect trails modules
287 %% @end
288 %% -------------------------------------------------------------------
289 collect_trails([], Acc) ->
290 614 Acc;
291 collect_trails([{Host, BasePath, Module} | T], Acc) ->
292
:-(
collect_trails([{Host, BasePath, Module, []} | T], Acc);
293 collect_trails([{_, _, Module, _} | T], Acc) ->
294 1530 case erlang:function_exported(Module, trails, 0) of
295 true ->
296 456 collect_trails(T, [Module | Acc]);
297 _ ->
298 1074 collect_trails(T, Acc)
299 end.
Line Hits Source