mirror of
https://github.com/processone/ejabberd.git
synced 2024-06-16 22:05:29 +02:00
![Badlop](/assets/img/avatar_default.png)
Thanks to: Alexey Shchepin for planning and developing MH in late 2007 Geoff Cant for improving during 2008 and 2009 Christophe Romain and Badlop for merging into master in 2010
404 lines
14 KiB
Erlang
404 lines
14 KiB
Erlang
%%%-------------------------------------------------------------------
|
|
%%% File : ejabberd_hosts.erl
|
|
%%% Author : Alexey Shchepin <alexey@process-one.net>
|
|
%%% Description : Synchronises running VHosts with the hosts table in the database.
|
|
%%% Created : 16 Nov 2007 by Alexey Shchepin <alexey@process-one.net>
|
|
%%%-------------------------------------------------------------------
|
|
-module(ejabberd_hosts).
|
|
|
|
-behaviour(gen_server).
|
|
|
|
%% External API
|
|
-export([start_link/0
|
|
,reload/0
|
|
]).
|
|
|
|
%% Host Registration API
|
|
-export([register/1
|
|
,register/2
|
|
,registered/1
|
|
,running/1
|
|
,registered/0
|
|
,remove/1
|
|
]).
|
|
|
|
%% Host control API
|
|
-export([start_host/1
|
|
,start_hosts/1
|
|
,stop_host/1
|
|
,stop_hosts/1
|
|
,load_host_cert/2
|
|
]).
|
|
|
|
%% Private utility functions
|
|
-export([get_hosts/1
|
|
,config_from_string/2
|
|
,get_host_config/2
|
|
,diff_hosts/0
|
|
,diff_hosts/1
|
|
,diff_hosts/2
|
|
,reload_hosts/0
|
|
,delete_host_config/1
|
|
]).
|
|
|
|
%% gen_server callbacks
|
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
|
|
terminate/2, code_change/3]).
|
|
|
|
-include("ejabberd.hrl").
|
|
-include("ejabberd_config.hrl").
|
|
-include_lib("stdlib/include/ms_transform.hrl").
|
|
|
|
-record(state, {state=wait_odbc,
|
|
odbc_wait_time=120}).
|
|
|
|
-define(RELOAD_INTERVAL, timer:seconds(60)).
|
|
-define(ODBC_STARTUP_TIME, 120). % 2minute limit for ODBC startup.
|
|
|
|
%%====================================================================
|
|
%% API
|
|
%%====================================================================
|
|
|
|
reload() ->
|
|
?MODULE ! reload.
|
|
|
|
%% Creates a vhost in the system.
|
|
register(Host) when is_list(Host) -> ?MODULE:register(Host, "").
|
|
register(Host, Config) when is_list(Host), is_list(Config) ->
|
|
true = jlib:is_nodename(Host),
|
|
ID = ejabberd_config:get_local_option(clusterid),
|
|
case ejabberd_odbc:sql_query(?MYNAME,
|
|
["INSERT INTO hosts (clusterid,host,config) VALUES (",
|
|
integer_to_list(ID), ", '",Host,"','",Config,"')"]) of
|
|
{updated, 1} ->
|
|
reload(),
|
|
ok;
|
|
{error, E} -> {error, E}
|
|
end.
|
|
|
|
%% Removes a vhost from the system,
|
|
%% XXX deleting all ODBC data.
|
|
remove(Host) when is_list(Host) ->
|
|
true = jlib:is_nodename(Host),
|
|
ID = ejabberd_config:get_local_option(clusterid),
|
|
case ejabberd_odbc:sql_query(?MYNAME,
|
|
["DELETE FROM hosts "
|
|
"WHERE clusterid=",
|
|
integer_to_list(ID), " AND host='",Host,"'"]) of
|
|
{updated, 1} ->
|
|
reload(),
|
|
ok;
|
|
{updated, 0} -> {error, no_such_host};
|
|
{error, E} -> {error, E}
|
|
end.
|
|
|
|
registered() ->
|
|
mnesia:dirty_select(local_config,
|
|
ets:fun2ms(fun (#local_config{key={Host, host}}) ->
|
|
Host
|
|
end)).
|
|
|
|
registered(Host) when is_list(Host) ->
|
|
case mnesia:dirty_read({local_config, {Host, host}}) of
|
|
[{local_config, {Host, host}, _}] -> true;
|
|
[] -> false
|
|
end.
|
|
|
|
running(global) -> true;
|
|
running(Host) ->
|
|
Routes = [H
|
|
|| {H, _, {apply, ejabberd_local, route}} <- ejabberd_router:read_route(Host),
|
|
H =:= Host],
|
|
Routes =/= [].
|
|
|
|
|
|
load_host_cert(Host, PemData) ->
|
|
File = cert_filename(Host),
|
|
ok = file:write_file(File, PemData),
|
|
configure_host_cert(Host, File).
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
|
|
%% Description: Starts the server
|
|
%%--------------------------------------------------------------------
|
|
start_link() ->
|
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
|
|
|
%%====================================================================
|
|
%% gen_server callbacks
|
|
%%====================================================================
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: init(Args) -> {ok, State} |
|
|
%% {ok, State, Timeout} |
|
|
%% ignore |
|
|
%% {stop, Reason}
|
|
%% Description: Initiates the server
|
|
%%--------------------------------------------------------------------
|
|
init([]) ->
|
|
configure_static_hosts(),
|
|
%% Wait up to 120 seconds for odbc to start
|
|
{ok, #state{state=wait_odbc,odbc_wait_time=?ODBC_STARTUP_TIME}, timer:seconds(1)}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
|
|
%% {reply, Reply, State, Timeout} |
|
|
%% {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, Reply, State} |
|
|
%% {stop, Reason, State}
|
|
%% Description: Handling call messages
|
|
%%--------------------------------------------------------------------
|
|
handle_call(_Request, _From, State) ->
|
|
Reply = ok,
|
|
{reply, Reply, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: handle_cast(Msg, State) -> {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, State}
|
|
%% Description: Handling cast messages
|
|
%%--------------------------------------------------------------------
|
|
handle_cast(_Msg, State) ->
|
|
{noreply, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: handle_info(Info, State) -> {noreply, State} |
|
|
%% {noreply, State, Timeout} |
|
|
%% {stop, Reason, State}
|
|
%% Description: Handling all non call/cast messages
|
|
%%--------------------------------------------------------------------
|
|
%% Wait for odbc to start.
|
|
handle_info(timeout, State = #state{state=wait_odbc,odbc_wait_time=N}) when N > 0 ->
|
|
case ejabberd_odbc:running(?MYNAME) of
|
|
true ->
|
|
?DEBUG("ejabberd_hosts: odbc now running.",[]),
|
|
self() ! reload,
|
|
timer:send_interval(?RELOAD_INTERVAL, reload),
|
|
{noreply, State#state{state=running,odbc_wait_time=0}};
|
|
false ->
|
|
{noreply,State#state{odbc_wait_time=N-1},timer:seconds(1)}
|
|
end;
|
|
handle_info(timeout, State=#state{state=running}) ->
|
|
?WARNING_MSG("Spurious timeout message when odbc is already running.", []),
|
|
{noreply, State};
|
|
|
|
handle_info(reload, State = #state{state=running}) ->
|
|
try reload_hosts()
|
|
catch
|
|
Class:Error ->
|
|
StackTrace = erlang:get_stacktrace(),
|
|
?ERROR_MSG("~p while synchonising running vhosts with database: ~p~n~p", [Class, Error, StackTrace])
|
|
end,
|
|
{noreply, State};
|
|
handle_info(reload, State = #state{state=wait_odbc}) ->
|
|
?ERROR_MSG("Tried to reload vhosts while waiting for odbc startup.", []),
|
|
handle_info(timeout, State);
|
|
|
|
handle_info(_Info, State) ->
|
|
{noreply, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Function: terminate(Reason, State) -> void()
|
|
%% Description: This function is called by a gen_server when it is about to
|
|
%% terminate. It should be the opposite of Module:init/1 and do any necessary
|
|
%% cleaning up. When it returns, the gen_server terminates with Reason.
|
|
%% The return value is ignored.
|
|
%%--------------------------------------------------------------------
|
|
terminate(_Reason, _State) ->
|
|
ok.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
|
|
%% Description: Convert process state when code is changed
|
|
%%--------------------------------------------------------------------
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
{ok, State}.
|
|
|
|
%%--------------------------------------------------------------------
|
|
%%% Internal functions
|
|
%%--------------------------------------------------------------------
|
|
|
|
reload_hosts() ->
|
|
reload_hosts(get_hosts(odbc)).
|
|
|
|
reload_hosts(NewHosts) ->
|
|
{AddedHosts,RemovedHosts} = diff_hosts(NewHosts),
|
|
%% Avoid removing permanent hosts (staticly configured hosts)
|
|
DeletedHosts = [H || H <- RemovedHosts,
|
|
ejabberd_config:get_host_option(H, permanent) =/= true],
|
|
AddHostConfig = lists:map(fun (Host) ->
|
|
{Host, get_host_config(odbc,Host)}
|
|
end, AddedHosts),
|
|
update_config(AddHostConfig,DeletedHosts),
|
|
ejabberd_config:add_global_option(hosts, NewHosts), % overwrite hosts list
|
|
stop_hosts(DeletedHosts),
|
|
start_hosts(AddedHosts),
|
|
ejabberd_local:refresh_iq_handlers(),
|
|
{DeletedHosts, AddedHosts}.
|
|
|
|
%% Apply the vhost changes (new, removed vhosts, configuration) to mnesia
|
|
update_config(AddHostConfig, RemoveHosts) ->
|
|
F = fun() ->
|
|
mnesia:write_lock_table(local_config),
|
|
lists:foreach(fun configure_new_host/1, AddHostConfig),
|
|
lists:foreach(fun delete_host_config/1, RemoveHosts),
|
|
ok
|
|
end,
|
|
{atomic, ok} = mnesia:transaction(F).
|
|
|
|
%% Write the configuration data for a new vhost to mnesia (currently only modules)
|
|
configure_new_host({H, Config}) when is_list(Config) ->
|
|
reconfigure_host_cert(H),
|
|
ejabberd_config:configure_host(H, Config).
|
|
|
|
%% Delete the mnesia config data for a vhost.
|
|
%% Needs to mirror any data we insert in configure_new_host (currently only modules)
|
|
delete_host_config(Host) ->
|
|
ejabberd_config:delete_host(Host).
|
|
|
|
%% Startup a list of vhosts
|
|
start_hosts([]) -> ok;
|
|
start_hosts(AddHosts) when is_list(AddHosts) ->
|
|
?DEBUG("ejabberd_hosts adding hosts: ~P", [AddHosts, 10]),
|
|
lists:foreach(fun start_host/1, AddHosts).
|
|
|
|
%% Start a single vhost (route, modules)
|
|
start_host(Host) ->
|
|
?DEBUG("Starting host ~p", [Host]),
|
|
ejabberd_router:register_route(Host, {apply, ejabberd_local, route}),
|
|
case ejabberd_config:get_local_option({modules, Host}) of
|
|
undefined -> ok;
|
|
Modules when is_list(Modules) ->
|
|
lists:foreach(
|
|
fun({Module, Args}) ->
|
|
gen_mod:start_module(Host, Module, Args)
|
|
end, Modules)
|
|
end,
|
|
case auth_method(Host) of
|
|
{host_method, HostMethod} ->
|
|
ejabberd_auth:start_method(Host, HostMethod);
|
|
_ -> ignore
|
|
end,
|
|
ok.
|
|
|
|
|
|
%% Shut down a list of vhosts.
|
|
stop_hosts([]) -> ok;
|
|
stop_hosts(RemoveHosts) when is_list(RemoveHosts)->
|
|
?DEBUG("ejabberd_hosts removing hosts: ~p", [RemoveHosts]),
|
|
lists:foreach(fun stop_host/1, RemoveHosts).
|
|
|
|
%% Shut down a single vhost. (Routes, modules)
|
|
stop_host(Host) ->
|
|
?DEBUG("Stopping host ~p", [Host]),
|
|
ejabberd_router:force_unregister_route(Host),
|
|
lists:foreach(fun(Module) ->
|
|
gen_mod:stop_module(Host, Module)
|
|
end, gen_mod:loaded_modules(Host)),
|
|
case auth_method(Host) of
|
|
use_global_auth_method -> ok;
|
|
{host_method, Method} ->
|
|
ejabberd_auth:stop_method(Host, Method)
|
|
end.
|
|
|
|
%% Get the current vhost list from a variety of sources (ODBC, internal)
|
|
get_hosts(ejabberd) -> ?MYHOSTS;
|
|
get_hosts(odbc) ->
|
|
ClusterID = ejabberd_config:get_local_option(clusterid),
|
|
case ejabberd_odbc:sql_query(
|
|
?MYNAME,
|
|
["select host from hosts where clusterid = ",
|
|
integer_to_list(ClusterID)]) of
|
|
{selected, ["host"], SHosts} ->
|
|
lists:map(fun ({Host}) ->
|
|
case jlib:nameprep(Host) of
|
|
error ->
|
|
erlang:error({bad_vhost_name, Host});
|
|
Name ->
|
|
Name
|
|
end
|
|
end, SHosts);
|
|
E ->
|
|
erlang:error({get_hosts_odbc_error, E})
|
|
end.
|
|
|
|
%% Retreive the text format config for host Host from ODBC and covert
|
|
%% it into a {host, Host, Config} tuple.
|
|
get_host_config(odbc, Host) ->
|
|
case ejabberd_odbc:sql_query(
|
|
?MYNAME,
|
|
["select config from hosts where host = '",
|
|
Host, "'"]) of
|
|
{selected, ["config"], [{Config}]} ->
|
|
config_from_string(Host, Config);
|
|
{selected, ["config"], []} ->
|
|
erlang:error({no_such_host, Host});
|
|
E ->
|
|
erlang:error({host_config_error, E})
|
|
end.
|
|
|
|
%% Convert a plaintext string into a host config tuple.
|
|
config_from_string(_Host, "") -> [];
|
|
config_from_string(_Host, Config) ->
|
|
{ok, Tokens, _} = erl_scan:string(Config),
|
|
case erl_parse:parse_term(Tokens) of
|
|
{ok, List} when is_list(List) ->
|
|
List;
|
|
E ->
|
|
erlang:error({bad_host_config, Config, E})
|
|
end.
|
|
|
|
diff_hosts() ->
|
|
diff_hosts(get_hosts(odbc)).
|
|
|
|
diff_hosts(NewHosts) ->
|
|
diff_hosts(NewHosts, get_hosts(ejabberd)).
|
|
|
|
%% Given the new list of vhosts and the old list, return the list of
|
|
%% hosts added since last time and the list of hosts that have been
|
|
%% removed.
|
|
diff_hosts(NewHosts, OldHosts) ->
|
|
RemoveHosts = OldHosts -- NewHosts,
|
|
AddHosts = NewHosts -- OldHosts,
|
|
{AddHosts,RemoveHosts}.
|
|
|
|
%% XXX - this should be part of auth, not hosts.
|
|
auth_method(Host) ->
|
|
case ejabberd_config:get_host_option(Host, auth_method) of
|
|
undefined ->
|
|
use_global_auth_method;
|
|
Other ->
|
|
{host_method, Other}
|
|
end.
|
|
|
|
configure_static_hosts() ->
|
|
?DEBUG("Node startup - configuring hosts: ~p", [?MYHOSTS]),
|
|
%% Add a null configuration for all MYHOSTS - this ensures
|
|
%% the 'I'm a host' term gets written to the config table.
|
|
%% We don't need any configuration options because these are
|
|
%% statically configured hosts already configured by ejabberd_config.
|
|
F = fun () ->
|
|
lists:foreach(fun (H) -> ejabberd_config:configure_host(H, [{permanent, true}]) end,
|
|
?MYHOSTS)
|
|
end,
|
|
mnesia:transaction(F).
|
|
|
|
cert_filename(Host) ->
|
|
Dir = ejabberd_config:get_local_option({domain_certdir, global}),
|
|
filename:join(Dir, Host ++ ".pem").
|
|
|
|
configure_host_cert(Host, File) ->
|
|
ejabberd_config:add_local_option({domain_certfile, Host}, File),
|
|
ok.
|
|
|
|
reconfigure_host_cert(Host) ->
|
|
File = cert_filename(Host),
|
|
case ejabberd_config:is_file_readable(File) of
|
|
true ->
|
|
ejabberd_config:mne_add_local_option({domain_certfile, Host}, File),
|
|
ok;
|
|
false ->
|
|
no_cert
|
|
end.
|