Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding mongoose_subdomain_core #3116

Merged
merged 11 commits into from
May 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/domain/mongoose_domain_core.erl
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ handle_delete(Domain) ->
ok;
[{Domain, HostType, _Source}] ->
ets:delete(?TABLE, Domain),
mongoose_subdomain_core:remove_domain(HostType, Domain),
mongoose_hooks:disable_domain(HostType, Domain),
ok
end.
Expand All @@ -212,6 +213,7 @@ handle_insert(Domain, HostType, Source) ->
{error, static};
[] ->
ets:insert_new(?TABLE, new_object(Domain, HostType, {dynamic, Source})),
mongoose_subdomain_core:add_domain(HostType, Domain),
ok;
[{Domain, HT, _Source}] when HT =:= HostType ->
ets:insert(?TABLE, new_object(Domain, HostType, {dynamic, Source})),
Expand Down
334 changes: 334 additions & 0 deletions src/domain/mongoose_subdomain_core.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
%% Generally, you should not call anything from this module.
%% Use mongoose_domain_api module instead.
-module(mongoose_subdomain_core).
-behaviour(gen_server).

DenysGonchar marked this conversation as resolved.
Show resolved Hide resolved
-include("mongoose_logger.hrl").
%% API
-export([start/0, stop/0]).
-export([start_link/0]).

-export([register_subdomain/3,
unregister_subdomain/2,
add_domain/2,
remove_domain/2,
sync/0]).

-export([get_host_type/1,
get_subdomain_info/1,
get_all_subdomains_for_domain/1]).

%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
code_change/3,
terminate/2]).

-ifdef(TEST).

-undef(LOG_ERROR).
-export([log_error/2]).
-define(LOG_ERROR(Error), ?MODULE:log_error(?FUNCTION_NAME, Error)).

-endif.

-define(SUBDOMAINS_TABLE, ?MODULE).
-define(REGISTRATION_TABLE, mongoose_subdomain_reg).

-type host_type() :: mongooseim:host_type().
-type domain() :: mongooseim:domain_name().
-type subdomain_pattern() :: mongoose_subdomain_utils:subdomain_pattern().
-type maybe_parent_domain() :: domain() | no_parent_domain.

-type reg_item() :: {{host_type(), subdomain_pattern()}, %% table key
Type :: fqdn | subdomain,
mongoose_packet_handler:t()}.

-record(subdomain_item, {host_type :: host_type() | '_',
subdomain :: domain() | '_', %% table key
subdomain_pattern :: subdomain_pattern() | '_',
parent_domain :: maybe_parent_domain() | '_',
packet_handler :: mongoose_packet_handler:t() | '_'}).

-type subdomain_item() :: #subdomain_item{}.

%% corresponds to the fields in #subdomain_item{} record
-type subdomain_info() :: #{host_type := host_type(),
Copy link
Member

@chrzaszcz chrzaszcz May 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two types: record and map look a bit odd to me. I understand that the record is used for ETS operations and the map is easier to use in the tests but... I would only have the record here.

Applying the Occam's razor makes me think that having only one type here would have the following benefits:

  • Easier to learn how it works
  • Easier to debug - one data structure is easier to follow
  • Less code, no unnecessary conversions
  • Easier to maintain in case the type needs to be extended

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to expose this record to the external code (not in this PR), it's not only about tests. I would say it was done not for the tests at all.

Copy link
Member

@chrzaszcz chrzaszcz May 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exposing the record beyond a a module would make more sense to me if there were a lot of data in the record taht we could hide. Otherwise it's just a lightweight and efficient data structure that you can just use directly instead of converting to a new structure that does not offer anything extra.

I don't want to start a long discussion here... it can stay as it is, in my view it's just unnecessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep it as it is for now.

subdomain := domain(),
subdomain_pattern := subdomain_pattern(),
parent_domain := maybe_parent_domain(),
packet_handler := mongoose_packet_handler:t()}.

-export_type([subdomain_info/0]).

%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------
-ifdef(TEST).
%% required for unit tests
start() ->
just_ok(gen_server:start({local, ?MODULE}, ?MODULE, [], [])).

stop() ->
gen_server:stop(?MODULE).

%% this interface is required only to detect errors in unit tests
log_error(_Function, _Error) -> ok.

-else.

start() ->
ChildSpec = {?MODULE, {?MODULE, start_link, []},
permanent, infinity, worker, [?MODULE]},
just_ok(supervisor:start_child(ejabberd_sup, ChildSpec)).

%% required for integration tests
stop() ->
supervisor:terminate_child(ejabberd_sup, ?MODULE),
supervisor:delete_child(ejabberd_sup, ?MODULE),
ok.

-endif.

start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

-spec register_subdomain(host_type(), subdomain_pattern(),
mongoose_packet_handler:t()) ->
ok | {error, already_registered | subdomain_already_exists}.
register_subdomain(HostType, SubdomainPattern, PacketHandler) ->
gen_server:call(?MODULE, {register, HostType, SubdomainPattern, PacketHandler}).

-spec unregister_subdomain(host_type(), subdomain_pattern()) -> ok.
unregister_subdomain(HostType, SubdomainPattern) ->
gen_server:call(?MODULE, {unregister, HostType, SubdomainPattern}).

-spec sync() -> ok.
sync() ->
gen_server:call(?MODULE, sync).

-spec add_domain(host_type(), domain()) -> ok.
add_domain(HostType, Domain) ->
gen_server:cast(?MODULE, {add_domain, HostType, Domain}).

-spec remove_domain(host_type(), domain()) -> ok.
remove_domain(HostType, Domain) ->
gen_server:cast(?MODULE, {remove_domain, HostType, Domain}).

-spec get_host_type(Subdomain :: domain()) -> {ok, host_type()} | {error, not_found}.
get_host_type(Subdomain) ->
case ets:lookup(?SUBDOMAINS_TABLE, Subdomain) of
[] ->
{error, not_found};
[#subdomain_item{host_type = HostType}] ->
{ok, HostType}
end.

-spec get_subdomain_info(Subdomain :: domain()) -> {ok, subdomain_info()} | {error, not_found}.
get_subdomain_info(Subdomain) ->
case ets:lookup(?SUBDOMAINS_TABLE, Subdomain) of
[] ->
{error, not_found};
[Item] ->
{ok, convert_subdomain_item_to_map(Item)}
end.

-spec get_all_subdomains_for_domain(Domain :: maybe_parent_domain()) -> [subdomain_info()].
%% if Domain param is set to no_parent_domain,
%% this function returns all the FQDN "subdomains".
get_all_subdomains_for_domain(Domain) ->
Pattern = #subdomain_item{parent_domain = Domain, _ = '_'},
Match = ets:match_object(?SUBDOMAINS_TABLE, Pattern),
[convert_subdomain_item_to_map(Item) || Item <- Match].

%%--------------------------------------------------------------------
%% gen_server callbacks
%%--------------------------------------------------------------------
init([]) ->
ets:new(?SUBDOMAINS_TABLE, [set, named_table, protected,
{keypos, #subdomain_item.subdomain},
{read_concurrency, true}]),
%% ?REGISTRATION_TABLE is protected only for traceability purposes
ets:new(?REGISTRATION_TABLE, [set, named_table, protected]),
%% no need for state, everything is kept in ETS tables
{ok, ok}.

handle_call({register, HostType, SubdomainPattern, PacketHandler}, From, State) ->
%% handle_register/4 must reply to the caller using gen_server:reply/2 interface
handle_register(HostType, SubdomainPattern, PacketHandler, From),
{noreply, State};
handle_call({unregister, HostType, SubdomainPattern}, _From, State) ->
Result = handle_unregister(HostType, SubdomainPattern),
{reply, Result, State};
handle_call(sync, _From, State) ->
{reply, ok, State};
handle_call(Request, From, State) ->
?UNEXPECTED_CALL(Request, From),
{reply, ok, State}.

handle_cast({add_domain, HostType, Domain}, State) ->
handle_add_domain(HostType, Domain),
{noreply, State};
handle_cast({remove_domain, HostType, Domain}, State) ->
handle_remove_domain(HostType, Domain),
{noreply, State};
handle_cast(Msg, State) ->
?UNEXPECTED_CAST(Msg),
{noreply, State}.

handle_info(Info, State) ->
?UNEXPECTED_INFO(Info),
{noreply, State}.

terminate(_Reason, _State) ->
ok.

code_change(_OldVsn, State, _Extra) ->
{ok, State}.

%%--------------------------------------------------------------------
%% local functions
%%--------------------------------------------------------------------
just_ok({ok, _}) -> ok;
just_ok(Other) -> Other.

-spec handle_register(host_type(), subdomain_pattern(),
mongoose_packet_handler:t(), any()) -> ok.
handle_register(HostType, SubdomainPattern, PacketHandler, From) ->
SubdomainType = mongoose_subdomain_utils:subdomain_type(SubdomainPattern),
RegItem = {{HostType, SubdomainPattern}, SubdomainType, PacketHandler},
case ets:insert_new(?REGISTRATION_TABLE, RegItem) of
true ->
case SubdomainType of
subdomain ->
Fn = fun(_HostType, Subdomain) ->
add_subdomain(RegItem, Subdomain)
end,
%% mongoose_domain_core:for_each_domain/2 can take quite a long,
%% reply before running it.
gen_server:reply(From, ok),
mongoose_domain_core:for_each_domain(HostType, Fn);
fqdn ->
Result = add_subdomain(RegItem, no_parent_domain),
gen_server:reply(From, Result)
end;
false ->
gen_server:reply(From, {error, already_registered})
end.

-spec handle_unregister(host_type(), subdomain_pattern()) -> ok.
handle_unregister(HostType, SubdomainPattern) ->
Pattern = #subdomain_item{subdomain_pattern = SubdomainPattern,
host_type = HostType, _ = '_'},
Match = ets:match_object(?SUBDOMAINS_TABLE, Pattern),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a huge list in the process heap. I think we would not unregister subdomains during normal operation though.

remove_subdomains(Match),
ets:delete(?REGISTRATION_TABLE, {HostType, SubdomainPattern}),
ok.

-spec handle_add_domain(host_type(), domain()) -> ok.
handle_add_domain(HostType, Domain) ->
check_domain_name(HostType, Domain),
%% even if the domain name check fails, it's not easy to solve this
%% collision. so the best thing we can do is to report it and just keep
%% the data in both ETS tables (domains and subdomains) for further
%% troubleshooting.
Match = ets:match_object(?REGISTRATION_TABLE, {{HostType, '_'}, subdomain, '_'}),
add_subdomains(Match, Domain).

-spec handle_remove_domain(host_type(), domain()) -> ok.
handle_remove_domain(HostType, Domain) ->
Pattern = #subdomain_item{parent_domain = Domain, host_type = HostType, _ = '_'},
Match = ets:match_object(?SUBDOMAINS_TABLE, Pattern),
remove_subdomains(Match).

-spec remove_subdomains([subdomain_item()]) -> ok.
remove_subdomains(SubdomainItems) ->
Fn = fun(#subdomain_item{host_type = HostType, subdomain = Subdomain}) ->
remove_subdomain(HostType, Subdomain)
end,
lists:foreach(Fn, SubdomainItems).

-spec remove_subdomain(host_type(), domain()) -> true.
remove_subdomain(HostType, Subdomain) ->
mongoose_hooks:disable_subdomain(HostType, Subdomain),
ets:delete(?SUBDOMAINS_TABLE, Subdomain).

-spec add_subdomains([reg_item()], domain()) -> ok.
add_subdomains(RegItems, Domain) ->
Fn = fun(RegItem) ->
add_subdomain(RegItem, Domain)
end,
lists:foreach(Fn, RegItems).

-spec add_subdomain(reg_item(), maybe_parent_domain()) -> ok | {error, already_registered}.
add_subdomain(RegItem, Domain) ->
#subdomain_item{subdomain = Subdomain} = Item = make_subdomain_item(RegItem, Domain),
case ets:insert_new(?SUBDOMAINS_TABLE, Item) of
true ->
check_subdomain_name(Item),
%% even if the subdomain name check fails, it's not easy to solve this
%% collision. so the best thing we can do is to report it and just keep
%% the data in both ETS tables (domains and subdomains) for further
%% troubleshooting.
ok;
false ->
case ets:lookup(?SUBDOMAINS_TABLE, Subdomain) of
[Item] ->
ok; %% exactly the same item is already inserted, it's fine.
[ExistingItem] ->
report_subdomains_collision(ExistingItem, Item),
{error, subdomain_already_exists}
end
end.

-spec make_subdomain_item(reg_item(), maybe_parent_domain()) -> subdomain_item().
make_subdomain_item({{HostType, SubdomainPattern}, Type, PacketHandler}, Domain) ->
Subdomain = case {Type, Domain} of
{fqdn, no_parent_domain} ->
%% not a subdomain, but FQDN
mongoose_subdomain_utils:get_fqdn(SubdomainPattern, <<"">>);
{subdomain, Domain} when is_binary(Domain) ->
mongoose_subdomain_utils:get_fqdn(SubdomainPattern, Domain)
end,
#subdomain_item{host_type = HostType, subdomain = Subdomain, parent_domain = Domain,
subdomain_pattern = SubdomainPattern, packet_handler = PacketHandler}.

-spec convert_subdomain_item_to_map(subdomain_item()) -> subdomain_info().
convert_subdomain_item_to_map(#subdomain_item{} = Item) ->
Fields = record_info(fields, subdomain_item),
[_ | Values] = tuple_to_list(Item),
KVList = lists:zip(Fields, Values),
maps:from_list(KVList).

-spec check_domain_name(mongooseim:host_type(), mongooseim:domain_name()) ->
boolean().
check_domain_name(_HostType, Domain) ->
case mongoose_subdomain_core:get_subdomain_info(Domain) of
{error, not_found} -> true;
{ok, _Info} ->
%% TODO: this is critical collision, and it must be reported properly
%% think about adding some metric, so devops can set some alarm for it
?LOG_ERROR(#{what => check_domain_name_failed, domain => Domain}),
false
end.

-spec check_subdomain_name(subdomain_item()) -> boolean().
check_subdomain_name(#subdomain_item{subdomain = Subdomain} = _SubdomainItem) ->
case mongoose_domain_core:get_host_type(Subdomain) of
{error, not_found} -> true;
{ok, _HostType} ->
%% TODO: this is critical collision, and it must be reported properly
%% think about adding some metric, so devops can set some alarm for it
?LOG_ERROR(#{what => check_subdomain_name_failed, subdomain => Subdomain}),
false
end.

-spec report_subdomains_collision(subdomain_item(), subdomain_item()) -> ok.
report_subdomains_collision(ExistingSubdomainItem, _NewSubdomainItem) ->
#subdomain_item{subdomain = Subdomain} = ExistingSubdomainItem,
%% TODO: this is critical collision, and it must be reported properly
%% think about adding some metric, so devops can set some alarm for it
?LOG_ERROR(#{what => subdomains_collision, subdomain => Subdomain}),
ok.
5 changes: 5 additions & 0 deletions src/domain/mongoose_subdomain_utils.erl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
%% API
-export([make_subdomain_pattern/1,
get_fqdn/2,
subdomain_type/1,
is_subdomain/3]).

-type subdomain_pattern() :: {fqdn | prefix, binary()}.
Expand All @@ -28,6 +29,10 @@ make_subdomain_pattern(ConfigOpt) when is_binary(ConfigOpt) ->
get_fqdn({fqdn, Subdomain}, _Domain) -> Subdomain;
get_fqdn({prefix, Prefix}, Domain) -> <<Prefix/binary, Domain/binary>>.

-spec subdomain_type(subdomain_pattern()) -> fqdn | subdomain.
subdomain_type({fqdn, _}) -> fqdn;
subdomain_type({prefix, _}) -> subdomain.

-spec is_subdomain(subdomain_pattern(),
Domain :: mongooseim:domain_name(),
Subdomain :: mongooseim:domain_name()) -> boolean().
Expand Down
11 changes: 10 additions & 1 deletion src/mongoose_hooks.erl
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
-export([c2s_remote_hook/5]).

-export([disable_domain/2,
disable_subdomain/2,
remove_domain/2,
node_cleanup/1]).

Expand Down Expand Up @@ -234,12 +235,20 @@ auth_failed(HostType, Server, Username) ->
ejabberd_hooks:run_for_host_type(auth_failed, HostType, ok, [Username, Server]).

-spec disable_domain(HostType, Domain) -> Result when
HostType :: binary(),
HostType :: mongooseim:host_type(),
Domain :: jid:lserver(),
Result :: ok.
disable_domain(HostType, Domain) ->
ejabberd_hooks:run_global(disable_domain, ok, [HostType, Domain]).


-spec disable_subdomain(HostType, Subdomain) -> Result when
HostType :: mongooseim:host_type(),
Subdomain :: jid:lserver(),
Result :: ok.
disable_subdomain(HostType, Subdomain) ->
ejabberd_hooks:run_global(disable_subdomain, ok, [HostType, Subdomain]).

-spec remove_domain(HostType, Domain) -> Result when
HostType :: binary(),
Domain :: jid:lserver(),
Expand Down
Loading