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