1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2000-2016. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20%%
21
22-module(httpd_manager).
23
24-include("httpd.hrl").
25
26-behaviour(gen_server).
27
28%% Application internal API
29-export([start/2, start_link/2, start_link/3, start_link/4,
30	 stop/1, reload/2]).
31-export([new_connection/1]).
32-export([config_match/3, config_match/4]).
33-export([block/2, block/3, unblock/1]).
34
35%% gen_server exports
36-export([init/1,
37	 handle_call/3, handle_cast/2, handle_info/2,
38	 terminate/2,
39         code_change/3]).
40
41-record(state,{socket_type  = ip_comm,
42	       config_file,
43	       config_db    = null,
44	       connection_sup,
45	       admin_state  = unblocked,
46	       blocker_ref  = undefined,
47	       blocking_from = undefined,
48	       shutdown_poller = undefined,
49	       status       = []}).
50%%%--------------------------------------------------------------------
51%%% Application internal API
52%%%--------------------------------------------------------------------
53
54%% Deprecated
55start(ConfigFile, ConfigList) ->
56    Port = proplists:get_value(port,ConfigList,80),
57    Addr = proplists:get_value(bind_address, ConfigList),
58    Profile = proplists:get_value(profile, ConfigList, default),
59    Name = make_name(Addr, Port, Profile),
60    gen_server:start({local,Name},?MODULE,
61		     [ConfigFile, ConfigList, 15000, Addr, Port],[]).
62
63%% Deprecated
64start_link(ConfigFile, ConfigList) ->
65    start_link(ConfigFile, ConfigList, 15000).
66
67start_link(ConfigFile, ConfigList, AcceptTimeout) ->
68    Port = proplists:get_value(port, ConfigList, 80),
69    Addr = proplists:get_value(bind_address, ConfigList),
70    Profile = proplists:get_value(profile, ConfigList, default),
71    Name = make_name(Addr, Port, Profile),
72
73    gen_server:start_link({local, Name},?MODULE,
74			  [ConfigFile, ConfigList,
75			   AcceptTimeout, Addr, Port],[]).
76
77start_link(ConfigFile, ConfigList, AcceptTimeout, ListenSocket) ->
78    Port = proplists:get_value(port, ConfigList, 80),
79    Addr = proplists:get_value(bind_address, ConfigList),
80    Profile = proplists:get_value(profile, ConfigList, default),
81    Name = make_name(Addr, Port, Profile),
82
83    gen_server:start_link({local, Name},?MODULE,
84			  [ConfigFile, ConfigList, AcceptTimeout, Addr,
85			   Port, ListenSocket],[]).
86stop(ServerRef) ->
87    call(ServerRef, stop).
88
89reload(ServerRef, Conf) ->
90    call(ServerRef, {reload, Conf}).
91
92block(ServerRef, Method) ->
93    block(ServerRef, Method, infinity).
94
95block(ServerRef, Method, Timeout) ->
96    call(ServerRef, {block, self(), Method, Timeout}).
97
98unblock(ServerRef) ->
99    call(ServerRef,{unblock, self()}).
100
101new_connection(Manager) ->
102    call(Manager, {new_connection, self()}).
103
104config_match(Port, Profile, Pattern) ->
105    config_match(undefined,Port, Profile, Pattern).
106config_match(Addr, Port, Profile, Pattern) ->
107    Name = httpd_util:make_name("httpd",Addr,Port, Profile),
108    call(whereis(Name), {config_match, Pattern}).
109
110%%%--------------------------------------------------------------------
111%%% gen_server callbacks functions
112%%%--------------------------------------------------------------------
113init([ConfigFile, ConfigList, AcceptTimeout, Addr, Port]) ->
114    process_flag(trap_exit, true),
115    case (catch do_init(ConfigFile, ConfigList, AcceptTimeout, Addr, Port)) of
116	{error, Reason} ->
117	    String = lists:flatten(
118		       io_lib:format("Failed initiating web server: "
119				     "~n~p"
120				     "~n~p"
121				     "~n", [ConfigFile, Reason])),
122	    error_logger:error_report(String),
123	    {stop, {error, Reason}};
124	{ok, State} ->
125	    {ok, State}
126    end;
127init([ConfigFile, ConfigList, AcceptTimeout, Addr, Port, ListenInfo]) ->
128    process_flag(trap_exit, true),
129    case (catch do_init(ConfigFile, ConfigList, AcceptTimeout,
130			Addr, Port, ListenInfo)) of
131	{error, Reason} ->
132	    String = lists:flatten(
133		       io_lib:format("Failed initiating web server: "
134				     "~n~p"
135				     "~n~p"
136				     "~n", [ConfigFile, Reason])),
137	    error_logger:error_report(String),
138	    {stop, {error, Reason}};
139	{ok, State} ->
140	    {ok, State}
141    end.
142
143do_init(ConfigFile, ConfigList, _AcceptTimeout, Addr, Port) ->
144    Sup = httpd_util:make_name("httpd_connection_sup", Addr, Port),
145    NewConfigFile = proplists:get_value(file, ConfigList, ConfigFile),
146    ConfigDB      = do_initial_store(ConfigList),
147    SocketType    = httpd_conf:lookup_socket_type(ConfigDB),
148    Status = [{max_conn,        0},
149	      {last_heavy_load, never},
150	      {last_connection, never}],
151	    State  = #state{socket_type = SocketType,
152			    config_file = NewConfigFile,
153			    config_db   = ConfigDB,
154			    connection_sup = Sup,
155			    status      = Status},
156    {ok, State}.
157
158do_init(ConfigFile, ConfigList, _AcceptTimeout, Addr, Port, _ListenInfo) ->
159    Sup = httpd_util:make_name("httpd_connection_sup", Addr, Port),
160    NewConfigFile = proplists:get_value(file, ConfigList, ConfigFile),
161    ConfigDB   = do_initial_store(ConfigList),
162    SocketType = httpd_conf:lookup_socket_type(ConfigDB),
163    Status = [{max_conn,0}, {last_heavy_load,never},
164	      {last_connection,never}],
165	    State  = #state{socket_type = SocketType,
166			    config_file = NewConfigFile,
167			    config_db   = ConfigDB,
168			    connection_sup = Sup,
169			    status      = Status},
170    {ok, State}.
171
172
173do_initial_store(ConfigList) ->
174    case httpd_conf:store(ConfigList) of
175	{ok, ConfigDB} ->
176	    ConfigDB;
177	{error, Reason} ->
178	    throw({error, Reason})
179    end.
180
181handle_call(stop, _From, State) ->
182    {stop, normal, ok, State};
183
184handle_call({config_match, Query}, _From, State) ->
185    Res = ets:match_object(State#state.config_db, Query),
186    {reply, Res, State};
187
188handle_call({reload, Conf}, _From, #state{admin_state = blocked} = State) ->
189    case handle_reload(Conf, State) of
190	{stop, Reply,S1} ->
191	    {stop, Reply, S1};
192	{_, Reply, S1} ->
193	    {reply,Reply,S1}
194    end;
195
196handle_call({reload, _}, _From, State) ->
197    {reply,{error,{invalid_admin_state,State#state.admin_state}},State};
198
199handle_call({block , Blocker, Mode, Timeout}, From,
200	    #state{admin_state = unblocked,
201		   connection_sup = CSup} = State) ->
202    Monitor = erlang:monitor(process, Blocker),
203    case count_children(CSup) of
204	0 ->
205	   %% Already in idle usage state => go directly to blocked
206	    {reply, ok, State#state{admin_state = blocked,
207				    blocker_ref = {Blocker, Monitor},
208				    blocking_from = From}};
209	_ ->
210	    handle_block(Mode, Timeout,
211			 State#state{blocker_ref = {Blocker, Monitor},
212				     blocking_from = From})
213    end;
214handle_call({block , _, _, _}, _, State) ->
215    {reply, {error, blocked}, State};
216
217handle_call({unblock, Blocker}, _, #state{blocker_ref = {Blocker, Monitor},
218					  admin_state = blocked} = State) ->
219
220    erlang:demonitor(Monitor),
221    {reply, ok,
222     State#state{admin_state = unblocked, blocker_ref = undefined}};
223
224handle_call({unblock, _}, _, State) ->
225    {reply, {error, only_blocker_may_unblock}, State};
226
227handle_call({new_connection, Pid}, _From, State) ->
228    {Status, NewState} = handle_new_connection(State, Pid),
229    {reply, Status, NewState};
230
231handle_call(Request, From, State) ->
232    String =
233	lists:flatten(
234	  io_lib:format("Unknown request "
235			"~n   ~p"
236			"~nto manager (~p)"
237			"~nfrom ~p",
238			[Request, self(), From])),
239    report_error(State,String),
240    {reply, ok, State}.
241
242handle_cast(Message, State) ->
243    String =
244	lists:flatten(
245	  io_lib:format("Unknown message "
246			"~n   ~p"
247			"~nto manager (~p)",
248			[Message, self()])),
249    report_error(State, String),
250    {noreply, State}.
251
252handle_info(connections_terminated, #state{admin_state = shutting_down,
253					   blocking_from = From} = State) ->
254    gen_server:reply(From, ok),
255    {noreply, State#state{admin_state = blocked, blocking_from = undefined}};
256handle_info(connections_terminated, State) ->
257    {noreply, State};
258
259handle_info({block_timeout, non_disturbing, Blocker},
260	    #state{admin_state = shutting_down,
261		   blocking_from = From,
262		   blocker_ref = {_, Monitor} = Blocker} = State) ->
263    erlang:demonitor(Monitor),
264    gen_server:reply(From, {error, timeout}),
265    {noreply, State#state{admin_state = unblocked, blocking_from = undefined,
266			  blocker_ref = undefined}};
267handle_info({block_timeout, disturbing, Blocker},
268	    #state{admin_state = shutting_down,
269		   blocking_from = From,
270		   blocker_ref = Blocker,
271		   connection_sup = Sup} = State) ->
272    SupPid = whereis(Sup),
273    shutdown_connections(SupPid),
274    gen_server:reply(From, ok),
275    {noreply, State#state{admin_state = blocked,
276			  blocking_from = undefined}};
277handle_info({block_timeout, _, _}, State) ->
278    {noreply, State};
279
280handle_info({'DOWN', _, process, Pid, _Info},
281	    #state{admin_state = Admin,
282		   blocker_ref = {Pid, Monitor}} = State) when
283      Admin =/= unblocked ->
284    erlang:demonitor(Monitor),
285    {noreply, State#state{admin_state = unblocked,
286			  blocking_from = undefined,
287			  blocker_ref = undefined}};
288handle_info({'DOWN', _, process, _, _}, State) ->
289    {noreply, State};
290
291handle_info({'EXIT', _, normal}, State) ->
292    {noreply, State};
293
294handle_info({'EXIT', _, shutdown}, State) ->
295    {stop, shutdown, State};
296
297handle_info(Info, State) ->
298    String =
299	lists:flatten(
300	  io_lib:format("Unknown info "
301			"~n   ~p"
302			"~nto manager (~p)",
303			[Info, self()])),
304    report_error(State, String),
305    {noreply, State}.
306
307terminate(_, #state{config_db = Db}) ->
308    httpd_conf:remove_all(Db),
309    ok.
310
311code_change({down,_ToVsn}, State, _Extra) ->
312    {ok,State};
313
314code_change(_FromVsn, State, _Extra) ->
315    {ok,State}.
316
317%%%--------------------------------------------------------------------
318%%% Internal functions
319%%%--------------------------------------------------------------------
320handle_new_connection(#state{admin_state = AdminState} = State, Handler) ->
321    UsageState = get_ustate(State),
322    handle_new_connection(UsageState, AdminState, State, Handler).
323
324handle_new_connection(_UsageState, unblocked,
325		      #state{config_db = Db, connection_sup = CSup} =
326			  State, _) ->
327    Max = httpd_util:lookup(Db, max_clients),
328    case count_children(CSup) of
329	Count when Count =< Max ->
330	    {{ok, accept}, State};
331	_ ->
332	    {{reject, busy}, State}
333    end;
334
335handle_new_connection(_UsageState, _AdminState, State, _Handler) ->
336    {{reject, blocked}, State}.
337
338handle_block(disturbing, infinity,
339	     #state{connection_sup = CSup,
340		    blocking_from = From} = State) ->
341    SupPid = whereis(CSup),
342    shutdown_connections(SupPid),
343    gen_server:reply(From, ok),
344    {noreply, State#state{admin_state = blocked,
345			  blocking_from = undefined}};
346handle_block(disturbing, Timeout, #state{connection_sup = CSup, blocker_ref = Blocker} = State) ->
347    Manager = self(),
348    spawn_link(fun() -> wait_for_shutdown(CSup, Manager) end),
349    erlang:send_after(Timeout, self(), {block_timeout, disturbing, Blocker}),
350    {noreply, State#state{admin_state = shutting_down}};
351
352handle_block(non_disturbing, infinity,
353	     #state{connection_sup = CSup} = State) ->
354    Manager = self(),
355    spawn_link(fun() -> wait_for_shutdown(CSup, Manager) end),
356    {noreply, State#state{admin_state = shutting_down}};
357
358handle_block(non_disturbing, Timeout,
359	     #state{connection_sup = CSup, blocker_ref = Blocker} = State) ->
360    Manager = self(),
361    spawn_link(fun() -> wait_for_shutdown(CSup, Manager) end),
362    erlang:send_after(Timeout, self(), {block_timeout, non_disturbing, Blocker}),
363    {noreply, State#state{admin_state = shutting_down}}.
364
365handle_reload(undefined, #state{config_file = undefined} = State) ->
366    {continue, {error, undefined_config_file}, State};
367handle_reload(undefined, #state{config_file = ConfigFile, admin_state = AdminState} = State) ->
368    try httpd:reload_config(ConfigFile, AdminState) of
369      Result ->
370            Result
371    catch throw:Err ->
372            {config_file, Err, State}
373    end;
374handle_reload(Config, State) ->
375    do_reload(Config, State).
376
377do_reload(Config, #state{config_db = Db} = State) ->
378    case (catch check_constant_values(Db, Config)) of
379	ok ->
380	    %% If something goes wrong between the remove
381	    %% and the store where fu-ed
382	    httpd_conf:remove_all(Db),
383	    case httpd_conf:store(Config) of
384		{ok, NewConfigDB} ->
385		    {continue, ok, State#state{config_db = NewConfigDB}};
386		Error ->
387		    {stop, Error, State}
388	    end;
389	Error ->
390	    {continue, Error, State}
391    end.
392
393check_constant_values(Db, Config) ->
394    %% Check port number
395    Port = httpd_util:lookup(Db,port),
396    case proplists:get_value(port,Config) of  %% MUST be equal
397	Port ->
398	    ok;
399	OtherPort ->
400	    throw({error,{port_number_changed,Port,OtherPort}})
401    end,
402
403    %% Check bind address
404    Addr = httpd_util:lookup(Db,bind_address),
405    case proplists:get_value(bind_address, Config) of  %% MUST be equal
406	Addr ->
407	    ok;
408	OtherAddr ->
409	    throw({error,{addr_changed,Addr,OtherAddr}})
410    end,
411
412    %% Check socket type
413    SockType = httpd_util:lookup(Db, socket_type),
414    case proplists:get_value(socket_type, Config) of  %% MUST be equal
415	SockType ->
416	    ok;
417	OtherSockType ->
418	    throw({error,{sock_type_changed,SockType,OtherSockType}})
419    end,
420    ok.
421
422
423%% get_ustate(State) -> idle | active | busy
424%%
425%% Retrieve the usage state of the HTTP server:
426%%   0 active connection            -> idle
427%%   max_clients active connections -> busy
428%%   Otherwise                      -> active
429%%
430get_ustate(State) ->
431    get_ustate(count_children(State#state.connection_sup),State).
432
433get_ustate(0,_State) ->
434    idle;
435get_ustate(ConnectionCnt,State) ->
436    ConfigDB = State#state.config_db,
437    case httpd_util:lookup(ConfigDB, max_clients, 150) of
438	ConnectionCnt ->
439	    busy;
440	_ ->
441	    active
442    end.
443
444make_name(Addr, Port, Profile) ->
445    httpd_util:make_name("httpd", Addr, Port, Profile).
446
447
448report_error(State,String) ->
449    Cdb = State#state.config_db,
450    error_logger:error_report(String),
451    mod_log:report_error(Cdb,String),
452    mod_disk_log:report_error(Cdb,String).
453
454call(ServerRef, Request) ->
455    try gen_server:call(ServerRef, Request, infinity)
456    catch
457	exit:_ ->
458	    {error, closed}
459    end.
460
461count_children(Sup) ->
462    Children = supervisor:count_children(whereis(Sup)),
463    proplists:get_value(workers, Children).
464
465shutdown_connections(Sup) ->
466    Children = [Child || {_,Child,_,_} <- supervisor:which_children(Sup)],
467    lists:foreach(fun(Pid) -> exit(Pid, kill) end,
468		  Children).
469
470wait_for_shutdown(CSup, Manager) ->
471    case count_children(CSup) of
472	0 ->
473	    Manager ! connections_terminated;
474	_ ->
475	    receive
476	    after 500 ->
477		    ok
478	    end,
479	    wait_for_shutdown(CSup, Manager)
480    end.
481
482