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([start_listener/1]). |
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/4, start_cowboy/2, stop_cowboy/1]). |
41 |
|
|
42 |
|
-ignore_xref([behaviour_info/1, process/1, ref/1, reload_dispatch/1, start_cowboy/2, |
43 |
|
start_cowboy/4, start_link/1, start_listener/2, start_listener/1, stop_cowboy/1]). |
44 |
|
|
45 |
|
-include("mongoose.hrl"). |
46 |
|
|
47 |
|
-type listener_options() :: #{port := inet:port_number(), |
48 |
|
ip_tuple := inet:ip_address(), |
49 |
|
ip_address := string(), |
50 |
|
ip_version := 4 | 6, |
51 |
|
proto := tcp, |
52 |
|
handlers := list(), |
53 |
|
transport := ranch:opts(), |
54 |
|
protocol := cowboy:opts(), |
55 |
|
atom() => any()}. |
56 |
|
|
57 |
|
-record(cowboy_state, {ref :: atom(), opts :: listener_options()}). |
58 |
|
|
59 |
|
%%-------------------------------------------------------------------- |
60 |
|
%% mongoose_listener API |
61 |
|
%%-------------------------------------------------------------------- |
62 |
|
|
63 |
|
-spec start_listener(listener_options()) -> ok. |
64 |
|
start_listener(Opts = #{proto := tcp}) -> |
65 |
28 |
ListenerId = mongoose_listener_config:listener_id(Opts), |
66 |
28 |
Ref = ref(ListenerId), |
67 |
28 |
ChildSpec = #{id => ListenerId, |
68 |
|
start => {?MODULE, start_link, [#cowboy_state{ref = Ref, opts = Opts}]}, |
69 |
|
restart => transient, |
70 |
|
shutdown => infinity, |
71 |
|
modules => [?MODULE]}, |
72 |
28 |
mongoose_listener_sup:start_child(ChildSpec), |
73 |
28 |
{ok, _} = start_cowboy(Ref, Opts), |
74 |
28 |
ok. |
75 |
|
|
76 |
|
reload_dispatch(Ref) -> |
77 |
:-( |
gen_server:call(Ref, reload_dispatch). |
78 |
|
|
79 |
|
%% @doc gen_server for handling shutdown when started via mongoose_listener |
80 |
|
-spec start_link(_) -> 'ignore' | {'error', _} | {'ok', pid()}. |
81 |
|
start_link(State) -> |
82 |
28 |
gen_server:start_link(?MODULE, State, []). |
83 |
|
|
84 |
|
init(State) -> |
85 |
28 |
process_flag(trap_exit, true), |
86 |
28 |
{ok, State}. |
87 |
|
|
88 |
|
handle_call(reload_dispatch, _From, #cowboy_state{ref = Ref, opts = Opts} = State) -> |
89 |
:-( |
reload_dispatch(Ref, Opts), |
90 |
:-( |
{reply, ok, State}; |
91 |
|
handle_call(_Request, _From, State) -> |
92 |
:-( |
{noreply, State}. |
93 |
|
|
94 |
|
handle_cast(_Request, State) -> |
95 |
:-( |
{noreply, State}. |
96 |
|
|
97 |
|
handle_info(_Info, State) -> |
98 |
:-( |
{noreply, State}. |
99 |
|
|
100 |
|
code_change(_OldVsn, State, _Extra) -> |
101 |
:-( |
{ok, State}. |
102 |
|
|
103 |
|
terminate(_Reason, State) -> |
104 |
28 |
stop_cowboy(State#cowboy_state.ref). |
105 |
|
|
106 |
|
-spec handler({integer(), inet:ip_address(), tcp}) -> list(). |
107 |
|
handler({Port, IP, tcp}) -> |
108 |
28 |
[inet_parse:ntoa(IP), <<"_">>, integer_to_list(Port)]. |
109 |
|
|
110 |
|
%%-------------------------------------------------------------------- |
111 |
|
%% cowboy_middleware callback |
112 |
|
%%-------------------------------------------------------------------- |
113 |
|
|
114 |
|
-spec execute(cowboy_req:req(), cowboy_middleware:env()) -> |
115 |
|
{ok, cowboy_req:req(), cowboy_middleware:env()}. |
116 |
|
execute(Req, Env) -> |
117 |
:-( |
case mongoose_config:lookup_opt(http_server_name) of |
118 |
|
{error, not_found} -> |
119 |
:-( |
{ok, Req, Env}; |
120 |
|
{ok, ServerName} -> |
121 |
:-( |
{ok, cowboy_req:set_resp_header(<<"server">>, ServerName, Req), Env} |
122 |
|
end. |
123 |
|
|
124 |
|
%%-------------------------------------------------------------------- |
125 |
|
%% Internal Functions |
126 |
|
%%-------------------------------------------------------------------- |
127 |
|
|
128 |
|
-spec start_cowboy(atom(), listener_options()) -> {ok, pid()} | {error, any()}. |
129 |
|
start_cowboy(Ref, Opts) -> |
130 |
28 |
start_cowboy(Ref, Opts, 20, 50). |
131 |
|
|
132 |
|
-spec start_cowboy(atom(), listener_options(), |
133 |
|
Retries :: non_neg_integer(), SleepTime :: non_neg_integer()) -> |
134 |
|
{ok, pid()} | {error, any()}. |
135 |
|
start_cowboy(Ref, Opts, 0, _) -> |
136 |
:-( |
do_start_cowboy(Ref, Opts); |
137 |
|
start_cowboy(Ref, Opts, Retries, SleepTime) -> |
138 |
28 |
case do_start_cowboy(Ref, Opts) of |
139 |
|
{error, eaddrinuse} -> |
140 |
:-( |
timer:sleep(SleepTime), |
141 |
:-( |
start_cowboy(Ref, Opts, Retries - 1, SleepTime); |
142 |
|
Other -> |
143 |
28 |
Other |
144 |
|
end. |
145 |
|
|
146 |
|
-spec do_start_cowboy(atom(), listener_options()) -> {ok, pid()} | {error, any()}. |
147 |
|
do_start_cowboy(Ref, Opts) -> |
148 |
28 |
#{ip_tuple := IPTuple, port := Port, handlers := Handlers0, |
149 |
|
transport := TransportOpts0, protocol := ProtocolOpts0} = Opts, |
150 |
28 |
Handlers = [ Handler#{ip_tuple => IPTuple, port => Port, proto => tcp} || Handler <- Handlers0 ], |
151 |
28 |
Routes = mongoose_http_handler:get_routes(Handlers), |
152 |
28 |
Dispatch = cowboy_router:compile(Routes), |
153 |
28 |
ProtocolOpts = ProtocolOpts0#{env => #{dispatch => Dispatch}}, |
154 |
28 |
TransportOpts = TransportOpts0#{socket_opts => [{port, Port}, {ip, IPTuple}]}, |
155 |
28 |
store_trails(Routes), |
156 |
28 |
case catch start_http_or_https(Opts, Ref, TransportOpts, ProtocolOpts) of |
157 |
|
{error, {{shutdown, |
158 |
|
{failed_to_start_child, ranch_acceptors_sup, |
159 |
|
{{badmatch, {error, eaddrinuse}}, _ }}}, _}} -> |
160 |
:-( |
{error, eaddrinuse}; |
161 |
|
Result -> |
162 |
28 |
Result |
163 |
|
end. |
164 |
|
|
165 |
|
start_http_or_https(#{tls := TLSOpts}, Ref, TransportOpts, ProtocolOpts) -> |
166 |
8 |
SSLOpts = just_tls:make_ssl_opts(TLSOpts), |
167 |
8 |
SocketOptsWithSSL = maps:get(socket_opts, TransportOpts) ++ SSLOpts, |
168 |
8 |
cowboy_start_https(Ref, TransportOpts#{socket_opts := SocketOptsWithSSL}, ProtocolOpts); |
169 |
|
start_http_or_https(#{}, Ref, TransportOpts, ProtocolOpts) -> |
170 |
20 |
cowboy_start_http(Ref, TransportOpts, ProtocolOpts). |
171 |
|
|
172 |
|
cowboy_start_http(Ref, TransportOpts, ProtocolOpts) -> |
173 |
20 |
ProtoOpts = add_common_middleware(ProtocolOpts), |
174 |
20 |
cowboy:start_clear(Ref, TransportOpts, ProtoOpts). |
175 |
|
|
176 |
|
cowboy_start_https(Ref, TransportOpts, ProtocolOpts) -> |
177 |
8 |
ProtoOpts = add_common_middleware(ProtocolOpts), |
178 |
8 |
cowboy:start_tls(Ref, TransportOpts, ProtoOpts). |
179 |
|
|
180 |
|
% We need to insert our middleware just before `cowboy_handler`, |
181 |
|
% so the injected response header is taken into account. |
182 |
|
add_common_middleware(Map = #{ middlewares := Middlewares }) -> |
183 |
:-( |
{Ms1, Ms2} = lists:splitwith(fun(Middleware) -> Middleware /= cowboy_handler end, Middlewares), |
184 |
:-( |
Map#{ middlewares := Ms1 ++ [?MODULE | Ms2] }; |
185 |
|
add_common_middleware(Map) -> |
186 |
28 |
Map#{ middlewares => [cowboy_router, ?MODULE, cowboy_handler] }. |
187 |
|
|
188 |
|
reload_dispatch(Ref, #{handlers := Handlers}) -> |
189 |
:-( |
Dispatch = cowboy_router:compile(mongoose_http_handler:get_routes(Handlers)), |
190 |
:-( |
cowboy:set_env(Ref, dispatch, Dispatch). |
191 |
|
|
192 |
|
stop_cowboy(Ref) -> |
193 |
28 |
cowboy:stop_listener(Ref). |
194 |
|
|
195 |
|
ref(Listener) -> |
196 |
28 |
Ref = handler(Listener), |
197 |
28 |
ModRef = [?MODULE_STRING, <<"_">>, Ref], |
198 |
28 |
list_to_atom(binary_to_list(iolist_to_binary(ModRef))). |
199 |
|
|
200 |
|
%% ------------------------------------------------------------------- |
201 |
|
%% @private |
202 |
|
%% @doc |
203 |
|
%% Store trails, this is needed to generate swagger documentation. |
204 |
|
%% Add to Trails each of modules where the trails behaviour is used. |
205 |
|
%% The modules must be added into `mongooseim.toml' in the `swagger' section. |
206 |
|
%% @end |
207 |
|
%% ------------------------------------------------------------------- |
208 |
|
store_trails(Routes) -> |
209 |
28 |
AllModules = lists:usort(lists:flatmap(fun({_Host, HostRoutes}) -> |
210 |
36 |
[Module || {_Path, Module, _Opts} <- HostRoutes] |
211 |
|
end, Routes)), |
212 |
28 |
TrailModules = lists:filter(fun(Module) -> |
213 |
120 |
mongoose_lib:is_exported(Module, trails, 0) |
214 |
|
end, AllModules), |
215 |
28 |
try |
216 |
28 |
trails:store(trails:trails(TrailModules)) |
217 |
|
catch Class:Reason:Stacktrace -> |
218 |
:-( |
?LOG_WARNING(#{what => store_trails_failed, |
219 |
:-( |
class => Class, reason => Reason, stacktrace => Stacktrace}) |
220 |
|
end. |