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