./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([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 325 ListenerId = mongoose_listener_config:listener_id(Opts),
66 325 Ref = ref(ListenerId),
67 325 ChildSpec = #{id => ListenerId,
68 start => {?MODULE, start_link, [#cowboy_state{ref = Ref, opts = Opts}]},
69 restart => transient,
70 shutdown => infinity,
71 modules => [?MODULE]},
72 325 mongoose_listener_sup:start_child(ChildSpec),
73 325 {ok, _} = start_cowboy(Ref, Opts),
74 325 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 325 gen_server:start_link(?MODULE, State, []).
83
84 init(State) ->
85 325 process_flag(trap_exit, true),
86 325 {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 325 stop_cowboy(State#cowboy_state.ref).
105
106 -spec handler({integer(), inet:ip_address(), tcp}) -> list().
107 handler({Port, IP, tcp}) ->
108 325 [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 4452 case mongoose_config:lookup_opt(http_server_name) of
118 {error, not_found} ->
119 4275 {ok, Req, Env};
120 {ok, ServerName} ->
121 177 {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 325 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 325 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 325 Other
144 end.
145
146 -spec do_start_cowboy(atom(), listener_options()) -> {ok, pid()} | {error, any()}.
147 do_start_cowboy(Ref, Opts) ->
148 325 #{ip_tuple := IPTuple, port := Port, handlers := Handlers0,
149 transport := TransportOpts0, protocol := ProtocolOpts0} = Opts,
150 325 Handlers = [ Handler#{ip_tuple => IPTuple, port => Port, proto => tcp} || Handler <- Handlers0 ],
151 325 Routes = mongoose_http_handler:get_routes(Handlers),
152 325 Dispatch = cowboy_router:compile(Routes),
153 325 ProtocolOpts = ProtocolOpts0#{env => #{dispatch => Dispatch}},
154 325 TransportOpts = TransportOpts0#{socket_opts => [{port, Port}, {ip, IPTuple}]},
155 325 store_trails(Routes),
156 325 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 325 Result
163 end.
164
165 start_http_or_https(#{tls := TLSOpts}, Ref, TransportOpts, ProtocolOpts) ->
166 93 SSLOpts = just_tls:make_cowboy_ssl_opts(TLSOpts),
167 93 SocketOptsWithSSL = maps:get(socket_opts, TransportOpts) ++ SSLOpts,
168 93 cowboy_start_https(Ref, TransportOpts#{socket_opts := SocketOptsWithSSL}, ProtocolOpts);
169 start_http_or_https(#{}, Ref, TransportOpts, ProtocolOpts) ->
170 232 cowboy_start_http(Ref, TransportOpts, ProtocolOpts).
171
172 cowboy_start_http(Ref, TransportOpts, ProtocolOpts) ->
173 232 ProtoOpts = add_common_middleware(ProtocolOpts),
174 232 cowboy:start_clear(Ref, TransportOpts, ProtoOpts).
175
176 cowboy_start_https(Ref, TransportOpts, ProtocolOpts) ->
177 93 ProtoOpts = add_common_middleware(ProtocolOpts),
178 93 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 325 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 325 cowboy:stop_listener(Ref).
194
195 ref(Listener) ->
196 325 Ref = handler(Listener),
197 325 ModRef = [?MODULE_STRING, <<"_">>, Ref],
198 325 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 325 AllModules = lists:usort(lists:flatmap(fun({_Host, HostRoutes}) ->
210 422 [Module || {_Path, Module, _Opts} <- HostRoutes]
211 end, Routes)),
212 325 TrailModules = lists:filter(fun(Module) ->
213 1410 mongoose_lib:is_exported(Module, trails, 0)
214 end, AllModules),
215 325 try
216 325 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.
Line Hits Source