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. |