diff --git a/src/domain/mongoose_domain_core.erl b/src/domain/mongoose_domain_core.erl index 3b3f442a6e8..6ec7d96ccab 100644 --- a/src/domain/mongoose_domain_core.erl +++ b/src/domain/mongoose_domain_core.erl @@ -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. @@ -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})), diff --git a/src/domain/mongoose_subdomain_core.erl b/src/domain/mongoose_subdomain_core.erl new file mode 100644 index 00000000000..e127ffe1d4c --- /dev/null +++ b/src/domain/mongoose_subdomain_core.erl @@ -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). + +-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(), + 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), + 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. diff --git a/src/domain/mongoose_subdomain_utils.erl b/src/domain/mongoose_subdomain_utils.erl index 5642f780093..224ebb24888 100644 --- a/src/domain/mongoose_subdomain_utils.erl +++ b/src/domain/mongoose_subdomain_utils.erl @@ -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()}. @@ -28,6 +29,10 @@ make_subdomain_pattern(ConfigOpt) when is_binary(ConfigOpt) -> get_fqdn({fqdn, Subdomain}, _Domain) -> Subdomain; get_fqdn({prefix, Prefix}, Domain) -> <>. +-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(). diff --git a/src/mongoose_hooks.erl b/src/mongoose_hooks.erl index 2877228cdf9..9a63df31971 100644 --- a/src/mongoose_hooks.erl +++ b/src/mongoose_hooks.erl @@ -155,6 +155,7 @@ -export([c2s_remote_hook/5]). -export([disable_domain/2, + disable_subdomain/2, remove_domain/2, node_cleanup/1]). @@ -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(), diff --git a/test/mongoose_subdomain_core_SUITE.erl b/test/mongoose_subdomain_core_SUITE.erl new file mode 100644 index 00000000000..702459c8350 --- /dev/null +++ b/test/mongoose_subdomain_core_SUITE.erl @@ -0,0 +1,644 @@ +-module(mongoose_subdomain_core_SUITE). + +-compile(export_all). + +-include_lib("eunit/include/eunit.hrl"). + +-define(STATIC_HOST_TYPE, <<"static type">>). +-define(STATIC_DOMAIN, <<"example.com">>). +-define(DYNAMIC_HOST_TYPE1, <<"dynamic type #1">>). +-define(DYNAMIC_HOST_TYPE2, <<"dynamic type #2">>). +-define(DYNAMIC_DOMAINS, [<<"localhost">>, <<"local.host">>]). +-define(STATIC_PAIRS, [{?STATIC_DOMAIN, ?STATIC_HOST_TYPE}]). +-define(ALLOWED_HOST_TYPES, [?DYNAMIC_HOST_TYPE1, ?DYNAMIC_HOST_TYPE2]). + +-define(assertEqualLists(L1, L2), ?assertEqual(lists:sort(L1), lists:sort(L2))). + +all() -> + [can_register_and_unregister_subdomain_for_static_host_type, + can_register_and_unregister_subdomain_for_dynamic_host_type_with_domains, + can_register_and_unregister_subdomain_for_dynamic_host_type_without_domains, + can_register_and_unregister_fqdn_for_static_host_type, + can_register_and_unregister_fqdn_for_dynamic_host_type_with_domains, + can_register_and_unregister_fqdn_for_dynamic_host_type_without_domains, + can_add_and_remove_domain, + can_get_host_type_and_subdomain_details, + handles_domain_removal_during_subdomain_registration, + prevents_double_subdomain_registration, + prevents_prefix_subdomain_overriding_by_prefix_subdomain, + prevents_fqdn_subdomain_overriding_by_prefix_subdomain, + prevents_prefix_subdomain_overriding_by_fqdn_subdomain, + prevents_fqdn_subdomain_overriding_by_fqdn_subdomain, + detects_domain_conflict_with_prefix_subdomain, + detects_domain_conflict_with_fqdn_subdomain]. + +init_per_suite(Config) -> + meck:new(mongoose_hooks, [no_link]), + meck:new(mongoose_subdomain_core, [no_link, passthrough]), + meck:expect(mongoose_hooks, disable_domain, fun(_, _) -> ok end), + meck:expect(mongoose_hooks, disable_subdomain, fun(_, _) -> ok end), + Config. + +end_per_suite(Config) -> + meck:unload(), + Config. + +init_per_testcase(_, Config) -> + %% mongoose_domain_core preconditions: + %% - one "static" host type with only one configured domain name + %% - one "dynamic" host type without any configured domain names + %% - one "dynamic" host type with two configured domain names + %% initial mongoose_subdomain_core conditions: + %% - no subdomains configured for any host type + ok = mongoose_domain_core:start(?STATIC_PAIRS, ?ALLOWED_HOST_TYPES), + ok = mongoose_subdomain_core:start(), + [mongoose_domain_core:insert(Domain, ?DYNAMIC_HOST_TYPE2, dummy_source) + || Domain <- ?DYNAMIC_DOMAINS], + [meck:reset(M) || M <- [mongoose_hooks, mongoose_subdomain_core]], + Config. + +end_per_testcase(_, Config) -> + mongoose_domain_core:stop(), + mongoose_subdomain_core:stop(), + Config. + +%%------------------------------------------------------------------- +%% normal test cases +%%------------------------------------------------------------------- +can_register_and_unregister_subdomain_for_static_host_type(_Config) -> + Handler = mongoose_packet_handler:new(?MODULE), + Pattern = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), + Subdomain = mongoose_subdomain_utils:get_fqdn(Pattern, ?STATIC_DOMAIN), + %% register one "prefix" subdomain for static host type. + %% check that ETS table contains expected subdomain and nothing else. + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?STATIC_HOST_TYPE, + Pattern, Handler)), + ?assertEqual([Subdomain], get_all_subdomains()), + ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?STATIC_HOST_TYPE, + Pattern)), + ?assertEqual([], get_all_subdomains()), + ?assertEqual([Subdomain], get_list_of_disabled_subdomains()), + no_collisions(). + +can_register_and_unregister_subdomain_for_dynamic_host_type_with_domains(_Config) -> + Handler = mongoose_packet_handler:new(?MODULE), + Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), + Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain2.@HOST@"), + Subdomains1 = [mongoose_subdomain_utils:get_fqdn(Pattern1, Domain) + || Domain <- ?DYNAMIC_DOMAINS], + Subdomains2 = [mongoose_subdomain_utils:get_fqdn(Pattern2, Domain) + || Domain <- ?DYNAMIC_DOMAINS], + %% register one "prefix" subdomain for dynamic host type with 2 domains. + %% check that ETS table contains all the expected subdomains and nothing else. + %% make a snapshot of subdomains ETS table and check its size. + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern1, Handler)), + ?assertEqualLists(Subdomains1, get_all_subdomains()), + %% register one more "prefix" subdomain for dynamic host type with 2 domains. + %% check that ETS table contains all the expected subdomains and nothing else. + %% check ETS table size. + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern2, Handler)), + ?assertEqualLists(Subdomains1 ++ Subdomains2, get_all_subdomains()), + %% check mongoose_subdomain_core:get_all_subdomains_for_domain/1 interface. + [DynamicDomain | _] = ?DYNAMIC_DOMAINS, + ?assertEqualLists( + [#{host_type => ?DYNAMIC_HOST_TYPE2, subdomain_pattern => Pattern1, + parent_domain => DynamicDomain, packet_handler => Handler, + subdomain => mongoose_subdomain_utils:get_fqdn(Pattern1, DynamicDomain)}, + #{host_type => ?DYNAMIC_HOST_TYPE2, subdomain_pattern => Pattern2, + parent_domain => DynamicDomain, packet_handler => Handler, + subdomain => mongoose_subdomain_utils:get_fqdn(Pattern2, DynamicDomain)}], + mongoose_subdomain_core:get_all_subdomains_for_domain(DynamicDomain)), + %% unregister (previously registered) subdomains one by one. + %% check that ETS table rolls back to the previously made snapshot. + ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern2)), + ?assertEqualLists(Subdomains1, get_all_subdomains()), + ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern1)), + ?assertEqual([], get_all_subdomains()), + ?assertEqualLists(Subdomains1 ++ Subdomains2, get_list_of_disabled_subdomains()), + no_collisions(). + +can_register_and_unregister_subdomain_for_dynamic_host_type_without_domains(_Config) -> + Handler = mongoose_packet_handler:new(?MODULE), + Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), + Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain2.@HOST@"), + %% register two "prefix" subdomains for dynamic host type with 0 domains. + %% check that ETS table doesn't contain any subdomains. + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern1, Handler)), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern2, Handler)), + ?assertEqual([], get_all_subdomains()), + %% unregister (previously registered) subdomains one by one. + ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern1)), + ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern2)), + ?assertEqual([], get_all_subdomains()), + ?assertEqual([], get_list_of_disabled_subdomains()), + no_collisions(). + +can_register_and_unregister_fqdn_for_static_host_type(_Config) -> + Pattern = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"), + Handler = mongoose_packet_handler:new(?MODULE), + %% register one FQDN subdomain for static host type. + %% check that ETS table contains the only expected subdomain. + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?STATIC_HOST_TYPE, + Pattern, Handler)), + ?assertEqual([<<"some.fqdn">>], get_all_subdomains()), + %% unregister subdomain. + ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?STATIC_HOST_TYPE, + Pattern)), + ?assertEqual([], get_all_subdomains()), + ?assertEqual([<<"some.fqdn">>], get_list_of_disabled_subdomains()), + no_collisions(). + +can_register_and_unregister_fqdn_for_dynamic_host_type_without_domains(_Config) -> + Pattern = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"), + Handler = mongoose_packet_handler:new(?MODULE), + %% register one FQDN subdomain for dynamic host type with 0 domains. + %% check that ETS table contains the only expected subdomain. + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern, Handler)), + ?assertEqual([<<"some.fqdn">>], get_all_subdomains()), + %% unregister subdomain. + ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern)), + ?assertEqual([], get_all_subdomains()), + ?assertEqual([<<"some.fqdn">>], get_list_of_disabled_subdomains()), + no_collisions(). + +can_register_and_unregister_fqdn_for_dynamic_host_type_with_domains(_Config) -> + Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"), + Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("another.fqdn"), + Handler = mongoose_packet_handler:new(?MODULE), + %% register one FQDN subdomain for dynamic host type with 2 domains. + %% check that ETS table contains all the expected subdomains and nothing else. + %% make a snapshot of subdomains ETS table. + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern1, Handler)), + ?assertEqual([<<"some.fqdn">>], get_all_subdomains()), + %% register one more FQDN subdomain for dynamic host type with 2 domains. + %% check mongoose_subdomain_core:get_all_subdomains_for_domain/1 interface + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern2, Handler)), + ?assertEqualLists([<<"some.fqdn">>, <<"another.fqdn">>], get_all_subdomains()), + ?assertEqualLists( + [#{host_type => ?DYNAMIC_HOST_TYPE2, parent_domain => no_parent_domain, + subdomain_pattern => Pattern1, packet_handler => Handler, + subdomain => <<"some.fqdn">>}, + #{host_type => ?DYNAMIC_HOST_TYPE2, parent_domain => no_parent_domain, + subdomain_pattern => Pattern2, packet_handler => Handler, + subdomain => <<"another.fqdn">>}], + mongoose_subdomain_core:get_all_subdomains_for_domain(no_parent_domain)), + %% unregister (previously registered) subdomains one by one. + %% check that ETS table rolls back to the previously made snapshot. + ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern2)), + ?assertEqual([<<"some.fqdn">>], get_all_subdomains()), + ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern1)), + ?assertEqual([], get_all_subdomains()), + ?assertEqualLists([<<"some.fqdn">>, <<"another.fqdn">>], + get_list_of_disabled_subdomains()), + no_collisions(). + +can_add_and_remove_domain(_Config) -> + Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), + Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain2.@HOST@"), + Pattern3 = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"), + Handler = mongoose_packet_handler:new(?MODULE), + Subdomains1 = [mongoose_subdomain_utils:get_fqdn(Pattern1, Domain) + || Domain <- ?DYNAMIC_DOMAINS], + Subdomains2 = [mongoose_subdomain_utils:get_fqdn(Pattern2, Domain) + || Domain <- ?DYNAMIC_DOMAINS], + ?assertEqual([], get_all_subdomains()), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern1, Handler)), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern2, Handler)), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern3, Handler)), + ?assertEqualLists([<<"some.fqdn">> | Subdomains1 ++ Subdomains2], + get_all_subdomains()), + [DynamicDomain | _] = ?DYNAMIC_DOMAINS, + mongoose_domain_core:delete(DynamicDomain), + ?assertEqualLists([<<"some.fqdn">> | tl(Subdomains1) ++ tl(Subdomains2)], + get_all_subdomains()), + ?assertEqualLists([hd(Subdomains1), hd(Subdomains2)], + get_list_of_disabled_subdomains()), + mongoose_domain_core:insert(DynamicDomain, ?DYNAMIC_HOST_TYPE2, dummy_source), + ?assertEqualLists([<<"some.fqdn">> | Subdomains1 ++ Subdomains2], + get_all_subdomains()), + no_collisions(). + +can_get_host_type_and_subdomain_details(_Config) -> + Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), + Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"), + Handler = mongoose_packet_handler:new(?MODULE), + Subdomain1 = mongoose_subdomain_utils:get_fqdn(Pattern1, ?STATIC_DOMAIN), + Subdomain2 = mongoose_subdomain_utils:get_fqdn(Pattern1, hd(?DYNAMIC_DOMAINS)), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?STATIC_HOST_TYPE, + Pattern1, Handler)), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern1, Handler)), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern2, Handler)), + mongoose_subdomain_core:sync(), + ?assertEqual({ok, ?STATIC_HOST_TYPE}, + mongoose_subdomain_core:get_host_type(Subdomain1)), + ?assertEqual({ok, ?DYNAMIC_HOST_TYPE1}, + mongoose_subdomain_core:get_host_type(<<"some.fqdn">>)), + ?assertEqual({ok, ?DYNAMIC_HOST_TYPE2}, + mongoose_subdomain_core:get_host_type(Subdomain2)), + ?assertEqual({error, not_found}, + mongoose_subdomain_core:get_host_type(<<"unknown.subdomain">>)), + ?assertEqual({ok, #{host_type => ?STATIC_HOST_TYPE, subdomain_pattern => Pattern1, + parent_domain => ?STATIC_DOMAIN, packet_handler => Handler, + subdomain => Subdomain1}}, + mongoose_subdomain_core:get_subdomain_info(Subdomain1)), + ?assertEqual({ok, #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern2, + parent_domain => no_parent_domain, packet_handler => Handler, + subdomain => <<"some.fqdn">>}}, + mongoose_subdomain_core:get_subdomain_info(<<"some.fqdn">>)), + ?assertEqual({ok, #{host_type => ?DYNAMIC_HOST_TYPE2, subdomain_pattern => Pattern1, + parent_domain => hd(?DYNAMIC_DOMAINS), packet_handler => Handler, + subdomain => Subdomain2}}, + mongoose_subdomain_core:get_subdomain_info(Subdomain2)), + ?assertEqual({error, not_found}, + mongoose_subdomain_core:get_subdomain_info(<<"unknown.subdomain">>)), + ok. + +handles_domain_removal_during_subdomain_registration(_Config) -> + %% NumOfDomains is just some big non-round number to ensure that more than 2 ets + %% selections are done during the call to mongoose_domain_core:for_each_domain/2. + %% currently max selection size is 100 domains. + NumOfDomains = 1234, + NumOfDomainsToRemove = 1234 div 4, + NewDomains = [<<"dummy_domain_", (integer_to_binary(N))/binary, ".localhost">> + || N <- lists:seq(1, NumOfDomains)], + [mongoose_domain_core:insert(Domain, ?DYNAMIC_HOST_TYPE1, dummy_src) + || Domain <- NewDomains], + meck:new(mongoose_domain_core, [passthrough]), + WrapperFn = make_wrapper_fn(NumOfDomainsToRemove * 2, NumOfDomainsToRemove), + meck:expect(mongoose_domain_core, for_each_domain, + fun(HostType, Fn) -> + meck:passthrough([HostType, WrapperFn(Fn)]) + end), + Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), + Handler = mongoose_packet_handler:new(?MODULE), + %% Note that mongoose_domain_core:for_each_domain/2 is used to register subdomain. + %% some domains are removed during subdomain registration, see make_wrapper_fn/2 + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern1, Handler)), + mongoose_subdomain_core:sync(), + %% try to add some domains second time, as this is also possible during + %% subdomain registration + AllDomains = mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1), + [RegisteredDomain1, RegisteredDomain2 | _] = AllDomains, + mongoose_subdomain_core:add_domain(?DYNAMIC_HOST_TYPE1, RegisteredDomain1), + mongoose_subdomain_core:add_domain(?DYNAMIC_HOST_TYPE1, RegisteredDomain2), + Subdomains = get_all_subdomains(), + ?assertEqual(NumOfDomains - NumOfDomainsToRemove, length(Subdomains)), + AllExpectedSubDomains = [mongoose_subdomain_utils:get_fqdn(Pattern1, Domain) + || Domain <- AllDomains], + ?assertEqualLists(AllExpectedSubDomains, Subdomains), + ?assertEqual(NumOfDomainsToRemove, + meck:num_calls(mongoose_hooks, disable_subdomain, 2)), + + no_collisions(), + meck:unload(mongoose_domain_core). + +prevents_double_subdomain_registration(_Config) -> + Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), + Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.fqdn"), + Handler = mongoose_packet_handler:new(?MODULE), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern1, Handler)), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern2, Handler)), + ?assertEqual({error, already_registered}, + mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern1, Handler)), + ?assertEqual({error, already_registered}, + mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern2, Handler)). + +%%------------------------------------------------------------------------------------- +%% test cases for subdomain names collisions. +%%------------------------------------------------------------------------------------- +%% There are three possible subdomain names collisions: +%% 1) Different domain/subdomain_pattern pairs produce one and the same subdomain. +%% 2) Attempt to register the same FQDN subdomain for 2 different host types. +%% 3) Domain/subdomain_pattern pair produces the same subdomain name as another +%% FQDN subdomain. +%% +%% Collisions of the first type can eliminated by allowing only one level subdomains, +%% e.g. ensuring that subdomain template corresponds to this regex "^[^.]*\.@HOST@$". +%% +%% Collisions of the second type are less critical as they can be detected during +%% init phase - they result in {error, subdomain_already_exists} return code, so +%% modules can detect it and crash at ?MODULE:start/2. +%% +%% Third type is hard to resolve in automatic way. One of the options is to ensure +%% that FQDN subdomains don't start with the same "prefix" as subdomain patterns. +%% +%% It's good idea to create a metric for such collisions, so devops can set some +%% alarm and react on it. +%% +%% The current behaviour rejects insertion of the conflicting subdomain, the original +%% subdomain must remain unchanged +%%------------------------------------------------------------------------------------- +prevents_prefix_subdomain_overriding_by_prefix_subdomain(_Config) -> + Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("sub.@HOST@"), + Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("sub.domain.@HOST@"), + Handler = mongoose_packet_handler:new(?MODULE), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern1, Handler)), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern2, Handler)), + %% one prefix subdomain conflicts with another prefix subdomain + mongoose_domain_core:insert(<<"test">>, ?DYNAMIC_HOST_TYPE1, dummy_src), + mongoose_domain_core:insert(<<"domain.test">>, ?DYNAMIC_HOST_TYPE1, dummy_src), + ?assertEqualLists( + [<<"sub.domain.domain.test">>, <<"sub.domain.test">>, <<"sub.test">>], + get_all_subdomains()), + ?assertEqualLists([<<"test">>, <<"domain.test">>], + mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)), + %% "test" domain is added first, so subdomain for this domain must remain unchanged + ExpectedSubdomainInfo = + #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern2, + parent_domain => <<"test">>, packet_handler => Handler, + subdomain => <<"sub.domain.test">>}, + ?assertEqual({ok, ExpectedSubdomainInfo}, + mongoose_subdomain_core:get_subdomain_info(<<"sub.domain.test">>)), + ?assertEqual([#{what => subdomains_collision, subdomain => <<"sub.domain.test">>}], + get_list_of_subdomain_collisions()), + no_domain_collisions(), + meck:reset(mongoose_subdomain_core), + %% check that removal of "domain.test" domain doesn't affect + %% "sub.domain.test" subdomain + mongoose_domain_core:delete(<<"domain.test">>), + ?assertEqual([<<"test">>], + mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)), + ?assertEqualLists([<<"sub.domain.test">>, <<"sub.test">>], get_all_subdomains()), + ?assertEqual({ok, ExpectedSubdomainInfo}, + mongoose_subdomain_core:get_subdomain_info(<<"sub.domain.test">>)), + ?assertEqual([<<"sub.domain.domain.test">>], get_list_of_disabled_subdomains()), + no_collisions(). + +prevents_fqdn_subdomain_overriding_by_prefix_subdomain(_Config) -> + Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), + Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.fqdn"), + Handler = mongoose_packet_handler:new(?MODULE), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern1, Handler)), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern2, Handler)), + %% FQDN subdomain conflicts with prefix subdomain + mongoose_domain_core:insert(<<"fqdn">>, ?DYNAMIC_HOST_TYPE1, dummy_src), + ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()), + ?assertEqual([<<"fqdn">>], + mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)), + %% FQDN subdomain is added first, so it must remain unchanged + ExpectedSubdomainInfo = + #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern2, + parent_domain => no_parent_domain, packet_handler => Handler, + subdomain => <<"subdomain.fqdn">>}, + ?assertEqual({ok, ExpectedSubdomainInfo}, + mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)), + ?assertEqual([#{what => subdomains_collision, subdomain => <<"subdomain.fqdn">>}], + get_list_of_subdomain_collisions()), + no_domain_collisions(), + meck:reset(mongoose_subdomain_core), + %% check that removal of "fqdn" domain doesn't affect FQDN subdomain + mongoose_domain_core:delete(<<"fqdn">>), + ?assertEqual([], mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)), + ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()), + ?assertEqual({ok, ExpectedSubdomainInfo}, + mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)), + no_collisions(). + +prevents_fqdn_subdomain_overriding_by_fqdn_subdomain(_Config) -> + Pattern = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.fqdn"), + Handler = mongoose_packet_handler:new(?MODULE), + %% FQDN subdomain conflicts with another FQDN subdomain + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern, Handler)), + ?assertEqual({error, subdomain_already_exists}, + mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern, Handler)), + %% FQDN subdomain for ?DYNAMIC_HOST_TYPE1 is registered first, so it must + %% remain unchanged + ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()), + ExpectedSubdomainInfo = + #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern, + parent_domain => no_parent_domain, packet_handler => Handler, + subdomain => <<"subdomain.fqdn">>}, + ?assertEqual({ok, ExpectedSubdomainInfo}, + mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)), + ?assertEqual([#{what => subdomains_collision, subdomain => <<"subdomain.fqdn">>}], + get_list_of_subdomain_collisions()), + no_domain_collisions(), + meck:reset(mongoose_subdomain_core), + %% check that unregistering FQDN subdomain for ?DYNAMIC_HOST_TYPE2 doesn't + %% affect FQDN subdomain for ?DYNAMIC_HOST_TYPE1 + ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2, + Pattern)), + ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()), + ?assertEqual({ok, ExpectedSubdomainInfo}, + mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)), + no_collisions(). + +prevents_prefix_subdomain_overriding_by_fqdn_subdomain(_Config) -> + Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), + Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.fqdn"), + Handler = mongoose_packet_handler:new(?MODULE), + %% FQDN subdomain conflicts with another FQDN subdomain + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern1, Handler)), + mongoose_domain_core:insert(<<"fqdn">>, ?DYNAMIC_HOST_TYPE1, dummy_src), + ?assertEqual({error, subdomain_already_exists}, + mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern2, Handler)), + %% FQDN subdomain for ?DYNAMIC_HOST_TYPE1 is registered first, so it must + %% remain unchanged + ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()), + ExpectedSubdomainInfo = + #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern1, + parent_domain => <<"fqdn">>, packet_handler => Handler, + subdomain => <<"subdomain.fqdn">>}, + ?assertEqual({ok, ExpectedSubdomainInfo}, + mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)), + ?assertEqual([#{what => subdomains_collision, subdomain => <<"subdomain.fqdn">>}], + get_list_of_subdomain_collisions()), + no_domain_collisions(), + meck:reset(mongoose_subdomain_core), + %% check that unregistering FQDN subdomain for ?DYNAMIC_HOST_TYPE2 doesn't + %% affect FQDN subdomain for ?DYNAMIC_HOST_TYPE1 + ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern2)), + ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()), + ?assertEqual({ok, ExpectedSubdomainInfo}, + mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)), + no_collisions(). + + +%%------------------------------------------------------------------------------------- +%% test cases for domain/subdomain names collisions. +%%------------------------------------------------------------------------------------- +%% There are two possible domain/subdomain names collisions: +%% 1) Domain/subdomain_pattern pair produces the same subdomain name as another +%% existing top level domain +%% 2) FQDN subdomain is the same as some registered top level domain +%% +%% The naive domain/subdomain registration rejection is probably a bad option: +%% * Domains and subdomains ETS tables are managed asynchronously, in addition to +%% that subdomains patterns registration is done async as well. This all leaves +%% room for various race conditions if we try to just make a verification and +%% prohibit domain/subdomain registration in case of any collisions. +%% * The only way to avoid such race conditions is to block all async. ETSs +%% editing during the validation process, but this can result in big delays +%% during the MIM initialisation phase. +%% * Also it's not clear how to interpret registration of the "prefix" based +%% subdomain patterns, should we block the registration of the whole pattern +%% or just only conflicting subdomains registration. Blocking of the whole +%% pattern requires generation and verification of all the subdomains (with +%% ETS blocking during that process), which depends on domains ETS size and +%% might take too long. +%% * And the last big issue with simple registration rejection approach, different +%% nodes in the cluster might have different registration sequence. So we may +%% end up in a situation when some nodes registered domain name as a subdomain, +%% while other nodes registered it as a top level domain. +%% +%% The better way is to prohibit registration of a top level domain if it is equal +%% to any of the FQDN subdomains or if beginning of domain name matches the prefix +%% of any subdomain template. In this case we don't need to verify subdomains at all, +%% verification of domain names against some limited number of subdomains patterns is +%% enough. And the only problem that we need to solve - mongooseim_domain_core must +%% be aware of all the subdomain patterns before it registers the first dynamic +%% domain. This would require minor configuration rework, e.g. tracking of subdomain +%% templates preprocessing (mongoose_subdomain_utils:make_subdomain_pattern/1 calls) +%% during TOML config parsing. +%% +%% It's good idea to create a metric for such collisions, so devops can set some +%% alarm and react on it. +%% +%% The current behaviour just ensures detection of the domain/subdomain names +%% collision, both (domain and subdomain) records remain unchanged in the +%% corresponding ETS tables +%%------------------------------------------------------------------------------------- +detects_domain_conflict_with_prefix_subdomain(_Config) -> + Pattern = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), + Handler = mongoose_packet_handler:new(?MODULE), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern, Handler)), + mongoose_domain_core:insert(<<"test.net">>, ?DYNAMIC_HOST_TYPE1, dummy_src), + %% without this sync call "subdomain.example.net" collision can be detected + %% twice, one time by check_subdomain_name/1 function and then second time + %% by check_domain_name/2. + mongoose_subdomain_core:sync(), + mongoose_domain_core:insert(<<"subdomain.test.net">>, ?DYNAMIC_HOST_TYPE2, dummy_src), + mongoose_domain_core:insert(<<"subdomain.test.org">>, ?DYNAMIC_HOST_TYPE2, dummy_src), + mongoose_domain_core:insert(<<"test.org">>, ?DYNAMIC_HOST_TYPE1, dummy_src), + ?assertEqualLists([<<"subdomain.test.org">>, <<"subdomain.test.net">>], + get_all_subdomains()), + ?assertEqualLists( + [<<"subdomain.test.org">>, <<"subdomain.test.net">> | ?DYNAMIC_DOMAINS], + mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE2)), + no_subdomain_collisions(), + ?assertEqual( + [#{what => check_domain_name_failed, domain => <<"subdomain.test.net">>}, + #{what => check_subdomain_name_failed, subdomain => <<"subdomain.test.org">>}], + get_list_of_domain_collisions()). + +detects_domain_conflict_with_fqdn_subdomain(_Config) -> + Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"), + Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("another.fqdn"), + Handler = mongoose_packet_handler:new(?MODULE), + + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern1, Handler)), + mongoose_domain_core:insert(<<"some.fqdn">>, ?DYNAMIC_HOST_TYPE1, dummy_src), + mongoose_domain_core:insert(<<"another.fqdn">>, ?DYNAMIC_HOST_TYPE1, dummy_src), + ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, + Pattern2, Handler)), + ?assertEqualLists([<<"some.fqdn">>, <<"another.fqdn">>], get_all_subdomains()), + ?assertEqualLists([<<"some.fqdn">>, <<"another.fqdn">>], + mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)), + no_subdomain_collisions(), + ?assertEqual( + [#{what => check_domain_name_failed, domain => <<"some.fqdn">>}, + #{what => check_subdomain_name_failed, subdomain => <<"another.fqdn">>}], + get_list_of_domain_collisions()). + +%%------------------------------------------------------------------- +%% internal functions +%%------------------------------------------------------------------- +get_all_subdomains() -> + mongoose_subdomain_core:sync(), + get_subdomains(). + +get_subdomains() -> + %% mongoose_subdomain_core table is indexed by subdomain name field + KeyPos = ets:info(mongoose_subdomain_core, keypos), + [element(KeyPos, Item) || Item <- ets:tab2list(mongoose_subdomain_core)]. + +make_wrapper_fn(N, M) when N > M -> + %% the wrapper function generates a new loop processing function + %% that pauses after after processing N domains, removes M of the + %% already processed domains and resumes after that. + fun(Fn) -> + put(number_of_iterations, 0), + fun(HostType, DomainName) -> + NumberOfIterations = get(number_of_iterations), + if + NumberOfIterations =:= N -> remove_some_domains(M); + true -> ok + end, + put(number_of_iterations, NumberOfIterations + 1), + Fn(HostType, DomainName) + end + end. + +remove_some_domains(N) -> + AllSubdomains = get_subdomains(), + [begin + {ok, Info} = mongoose_subdomain_core:get_subdomain_info(Subdomain), + ParentDomain = maps:get(parent_domain, Info), + mongoose_domain_core:delete(ParentDomain) + end || Subdomain <- lists:sublist(AllSubdomains, N)]. + +no_collisions() -> + no_domain_collisions(), + no_subdomain_collisions(). + +no_domain_collisions() -> + Hist = meck:history(mongoose_subdomain_core), + Errors = [Call || {_P, {_M, log_error = _F, [From, _] = _A}, _R} = Call <- Hist, + From =:= check_subdomain_name orelse From =:= check_domain_name], + ?assertEqual([], Errors). + +get_list_of_domain_collisions() -> + Hist = meck:history(mongoose_subdomain_core), + [Error || {_Pid, {_Mod, log_error = _Func, [From, Error] = _Args}, _Result} <- Hist, + From =:= check_subdomain_name orelse From =:= check_domain_name]. + +no_subdomain_collisions() -> + Hist = meck:history(mongoose_subdomain_core), + Errors = [Call || {_P, {_M, log_error = _F, [From, _] = _A}, _R} = Call <- Hist, + From =:= report_subdomains_collision], + ?assertEqual([], Errors). + +get_list_of_subdomain_collisions() -> + Hist = meck:history(mongoose_subdomain_core), + [Error || {_Pid, {_Mod, log_error = _Func, [From, Error] = _Args}, _Result} <- Hist, + From =:= report_subdomains_collision]. + +get_list_of_disabled_subdomains() -> + History = meck:history(mongoose_hooks), + [lists:nth(2, Args) %% Subdomain is the second argument + || {_Pid, {_Mod, disable_subdomain = _Func, Args}, _Result} <- History].