1: -module(mongoose_subdomain_core_SUITE). 2: 3: -compile([export_all, nowarn_export_all]). 4: 5: -include_lib("eunit/include/eunit.hrl"). 6: 7: -define(STATIC_HOST_TYPE, <<"static type">>). 8: -define(STATIC_DOMAIN, <<"example.com">>). 9: -define(DYNAMIC_HOST_TYPE1, <<"dynamic type #1">>). 10: -define(DYNAMIC_HOST_TYPE2, <<"dynamic type #2">>). 11: -define(DYNAMIC_DOMAINS, [<<"localhost">>, <<"local.host">>]). 12: -define(STATIC_PAIRS, [{?STATIC_DOMAIN, ?STATIC_HOST_TYPE}]). 13: -define(ALLOWED_HOST_TYPES, [?DYNAMIC_HOST_TYPE1, ?DYNAMIC_HOST_TYPE2]). 14: 15: -define(assertEqualLists(L1, L2), ?assertEqual(lists:sort(L1), lists:sort(L2))). 16: 17: all() -> 18: [can_register_and_unregister_subdomain_for_static_host_type, 19: can_register_and_unregister_subdomain_for_dynamic_host_type_with_domains, 20: can_register_and_unregister_subdomain_for_dynamic_host_type_without_domains, 21: can_register_and_unregister_fqdn_for_static_host_type, 22: can_register_and_unregister_fqdn_for_dynamic_host_type_with_domains, 23: can_register_and_unregister_fqdn_for_dynamic_host_type_without_domains, 24: can_add_and_remove_domain, 25: can_get_host_type_and_subdomain_details, 26: handles_domain_removal_during_subdomain_registration, 27: prevents_double_subdomain_registration, 28: prevents_prefix_subdomain_overriding_by_prefix_subdomain, 29: prevents_fqdn_subdomain_overriding_by_prefix_subdomain, 30: prevents_prefix_subdomain_overriding_by_fqdn_subdomain, 31: prevents_fqdn_subdomain_overriding_by_fqdn_subdomain, 32: detects_domain_conflict_with_prefix_subdomain, 33: detects_domain_conflict_with_fqdn_subdomain]. 34: 35: init_per_testcase(TestCase, Config) -> 36: %% mongoose_domain_core preconditions: 37: %% - one "static" host type with only one configured domain name 38: %% - one "dynamic" host type without any configured domain names 39: %% - one "dynamic" host type with two configured domain names 40: %% initial mongoose_subdomain_core conditions: 41: %% - no subdomains configured for any host type 42: mongooseim_helper:start_link_loaded_hooks(), 43: mongoose_domain_sup:start_link(?STATIC_PAIRS, ?ALLOWED_HOST_TYPES), 44: [mongoose_domain_core:insert(Domain, ?DYNAMIC_HOST_TYPE2, dummy_source) 45: || Domain <- ?DYNAMIC_DOMAINS], 46: setup_meck(TestCase), 47: Config. 48: 49: end_per_testcase(_, Config) -> 50: meck:unload(), 51: Config. 52: 53: %%------------------------------------------------------------------- 54: %% normal test cases 55: %%------------------------------------------------------------------- 56: can_register_and_unregister_subdomain_for_static_host_type(_Config) -> 57: Handler = mongoose_packet_handler:new(?MODULE), 58: Pattern = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), 59: Subdomain = mongoose_subdomain_utils:get_fqdn(Pattern, ?STATIC_DOMAIN), 60: %% register one "prefix" subdomain for static host type. 61: %% check that ETS table contains expected subdomain and nothing else. 62: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?STATIC_HOST_TYPE, 63: Pattern, Handler)), 64: ?assertEqual([Subdomain], get_all_subdomains()), 65: ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?STATIC_HOST_TYPE, 66: Pattern)), 67: ?assertEqual([], get_all_subdomains()), 68: ?assertEqual([Subdomain], get_list_of_disabled_subdomains()), 69: no_collisions(). 70: 71: can_register_and_unregister_subdomain_for_dynamic_host_type_with_domains(_Config) -> 72: Handler = mongoose_packet_handler:new(?MODULE), 73: Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), 74: Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain2.@HOST@"), 75: Subdomains1 = [mongoose_subdomain_utils:get_fqdn(Pattern1, Domain) 76: || Domain <- ?DYNAMIC_DOMAINS], 77: Subdomains2 = [mongoose_subdomain_utils:get_fqdn(Pattern2, Domain) 78: || Domain <- ?DYNAMIC_DOMAINS], 79: %% register one "prefix" subdomain for dynamic host type with 2 domains. 80: %% check that ETS table contains all the expected subdomains and nothing else. 81: %% make a snapshot of subdomains ETS table and check its size. 82: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, 83: Pattern1, Handler)), 84: ?assertEqualLists(Subdomains1, get_all_subdomains()), 85: %% register one more "prefix" subdomain for dynamic host type with 2 domains. 86: %% check that ETS table contains all the expected subdomains and nothing else. 87: %% check ETS table size. 88: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, 89: Pattern2, Handler)), 90: ?assertEqualLists(Subdomains1 ++ Subdomains2, get_all_subdomains()), 91: %% check mongoose_subdomain_core:get_all_subdomains_for_domain/1 interface. 92: [DynamicDomain | _] = ?DYNAMIC_DOMAINS, 93: HostTypeExtra = #{host_type => ?DYNAMIC_HOST_TYPE2}, 94: HandlerWithHostType = mongoose_packet_handler:add_extra(Handler, HostTypeExtra), 95: ?assertEqualLists( 96: [#{host_type => ?DYNAMIC_HOST_TYPE2, subdomain_pattern => Pattern1, 97: parent_domain => DynamicDomain, packet_handler => HandlerWithHostType, 98: subdomain => mongoose_subdomain_utils:get_fqdn(Pattern1, DynamicDomain)}, 99: #{host_type => ?DYNAMIC_HOST_TYPE2, subdomain_pattern => Pattern2, 100: parent_domain => DynamicDomain, packet_handler => HandlerWithHostType, 101: subdomain => mongoose_subdomain_utils:get_fqdn(Pattern2, DynamicDomain)}], 102: mongoose_subdomain_core:get_all_subdomains_for_domain(DynamicDomain)), 103: %% unregister (previously registered) subdomains one by one. 104: %% check that ETS table rolls back to the previously made snapshot. 105: ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2, 106: Pattern2)), 107: ?assertEqualLists(Subdomains1, get_all_subdomains()), 108: ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2, 109: Pattern1)), 110: ?assertEqual([], get_all_subdomains()), 111: ?assertEqualLists(Subdomains1 ++ Subdomains2, get_list_of_disabled_subdomains()), 112: no_collisions(). 113: 114: can_register_and_unregister_subdomain_for_dynamic_host_type_without_domains(_Config) -> 115: Handler = mongoose_packet_handler:new(?MODULE), 116: Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), 117: Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain2.@HOST@"), 118: %% register two "prefix" subdomains for dynamic host type with 0 domains. 119: %% check that ETS table doesn't contain any subdomains. 120: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 121: Pattern1, Handler)), 122: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 123: Pattern2, Handler)), 124: ?assertEqual([], get_all_subdomains()), 125: %% unregister (previously registered) subdomains one by one. 126: ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE1, 127: Pattern1)), 128: ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE1, 129: Pattern2)), 130: ?assertEqual([], get_all_subdomains()), 131: ?assertEqual([], get_list_of_disabled_subdomains()), 132: no_collisions(). 133: 134: can_register_and_unregister_fqdn_for_static_host_type(_Config) -> 135: Pattern = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"), 136: Handler = mongoose_packet_handler:new(?MODULE), 137: %% register one FQDN subdomain for static host type. 138: %% check that ETS table contains the only expected subdomain. 139: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?STATIC_HOST_TYPE, 140: Pattern, Handler)), 141: ?assertEqual([<<"some.fqdn">>], get_all_subdomains()), 142: %% unregister subdomain. 143: ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?STATIC_HOST_TYPE, 144: Pattern)), 145: ?assertEqual([], get_all_subdomains()), 146: ?assertEqual([<<"some.fqdn">>], get_list_of_disabled_subdomains()), 147: no_collisions(). 148: 149: can_register_and_unregister_fqdn_for_dynamic_host_type_without_domains(_Config) -> 150: Pattern = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"), 151: Handler = mongoose_packet_handler:new(?MODULE), 152: %% register one FQDN subdomain for dynamic host type with 0 domains. 153: %% check that ETS table contains the only expected subdomain. 154: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 155: Pattern, Handler)), 156: ?assertEqual([<<"some.fqdn">>], get_all_subdomains()), 157: %% unregister subdomain. 158: ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE1, 159: Pattern)), 160: ?assertEqual([], get_all_subdomains()), 161: ?assertEqual([<<"some.fqdn">>], get_list_of_disabled_subdomains()), 162: no_collisions(). 163: 164: can_register_and_unregister_fqdn_for_dynamic_host_type_with_domains(_Config) -> 165: Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"), 166: Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("another.fqdn"), 167: Handler = mongoose_packet_handler:new(?MODULE), 168: %% register one FQDN subdomain for dynamic host type with 2 domains. 169: %% check that ETS table contains all the expected subdomains and nothing else. 170: %% make a snapshot of subdomains ETS table. 171: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, 172: Pattern1, Handler)), 173: ?assertEqual([<<"some.fqdn">>], get_all_subdomains()), 174: %% register one more FQDN subdomain for dynamic host type with 2 domains. 175: %% check mongoose_subdomain_core:get_all_subdomains_for_domain/1 interface 176: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, 177: Pattern2, Handler)), 178: ?assertEqualLists([<<"some.fqdn">>, <<"another.fqdn">>], get_all_subdomains()), 179: HostTypeExtra = #{host_type => ?DYNAMIC_HOST_TYPE2}, 180: HandlerWithHostType = mongoose_packet_handler:add_extra(Handler, HostTypeExtra), 181: ?assertEqualLists( 182: [#{host_type => ?DYNAMIC_HOST_TYPE2, parent_domain => no_parent_domain, 183: subdomain_pattern => Pattern1, packet_handler => HandlerWithHostType, 184: subdomain => <<"some.fqdn">>}, 185: #{host_type => ?DYNAMIC_HOST_TYPE2, parent_domain => no_parent_domain, 186: subdomain_pattern => Pattern2, packet_handler => HandlerWithHostType, 187: subdomain => <<"another.fqdn">>}], 188: mongoose_subdomain_core:get_all_subdomains_for_domain(no_parent_domain)), 189: %% unregister (previously registered) subdomains one by one. 190: %% check that ETS table rolls back to the previously made snapshot. 191: ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2, 192: Pattern2)), 193: ?assertEqual([<<"some.fqdn">>], get_all_subdomains()), 194: ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2, 195: Pattern1)), 196: ?assertEqual([], get_all_subdomains()), 197: ?assertEqualLists([<<"some.fqdn">>, <<"another.fqdn">>], 198: get_list_of_disabled_subdomains()), 199: no_collisions(). 200: 201: can_add_and_remove_domain(_Config) -> 202: Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), 203: Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain2.@HOST@"), 204: Pattern3 = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"), 205: Handler = mongoose_packet_handler:new(?MODULE), 206: Subdomains1 = [mongoose_subdomain_utils:get_fqdn(Pattern1, Domain) 207: || Domain <- ?DYNAMIC_DOMAINS], 208: Subdomains2 = [mongoose_subdomain_utils:get_fqdn(Pattern2, Domain) 209: || Domain <- ?DYNAMIC_DOMAINS], 210: ?assertEqual([], get_all_subdomains()), 211: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, 212: Pattern1, Handler)), 213: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, 214: Pattern2, Handler)), 215: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, 216: Pattern3, Handler)), 217: ?assertEqualLists([<<"some.fqdn">> | Subdomains1 ++ Subdomains2], 218: get_all_subdomains()), 219: [DynamicDomain | _] = ?DYNAMIC_DOMAINS, 220: mongoose_domain_core:delete(DynamicDomain), 221: ?assertEqualLists([<<"some.fqdn">> | tl(Subdomains1) ++ tl(Subdomains2)], 222: get_all_subdomains()), 223: ?assertEqualLists([hd(Subdomains1), hd(Subdomains2)], 224: get_list_of_disabled_subdomains()), 225: mongoose_domain_core:insert(DynamicDomain, ?DYNAMIC_HOST_TYPE2, dummy_source), 226: ?assertEqualLists([<<"some.fqdn">> | Subdomains1 ++ Subdomains2], 227: get_all_subdomains()), 228: no_collisions(). 229: 230: can_get_host_type_and_subdomain_details(_Config) -> 231: Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), 232: Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"), 233: Handler = mongoose_packet_handler:new(?MODULE), 234: Subdomain1 = mongoose_subdomain_utils:get_fqdn(Pattern1, ?STATIC_DOMAIN), 235: Subdomain2 = mongoose_subdomain_utils:get_fqdn(Pattern1, hd(?DYNAMIC_DOMAINS)), 236: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?STATIC_HOST_TYPE, 237: Pattern1, Handler)), 238: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, 239: Pattern1, Handler)), 240: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 241: Pattern2, Handler)), 242: mongoose_subdomain_core:sync(), 243: ?assertEqual({ok, ?STATIC_HOST_TYPE}, 244: mongoose_subdomain_core:get_host_type(Subdomain1)), 245: ?assertEqual({ok, ?DYNAMIC_HOST_TYPE1}, 246: mongoose_subdomain_core:get_host_type(<<"some.fqdn">>)), 247: ?assertEqual({ok, ?DYNAMIC_HOST_TYPE2}, 248: mongoose_subdomain_core:get_host_type(Subdomain2)), 249: ?assertEqual({error, not_found}, 250: mongoose_subdomain_core:get_host_type(<<"unknown.subdomain">>)), 251: HostTypeExtra1 = #{host_type => ?STATIC_HOST_TYPE}, 252: HandlerWithHostType1 = mongoose_packet_handler:add_extra(Handler, HostTypeExtra1), 253: ?assertEqual({ok, #{host_type => ?STATIC_HOST_TYPE, subdomain_pattern => Pattern1, 254: parent_domain => ?STATIC_DOMAIN, subdomain => Subdomain1, 255: packet_handler => HandlerWithHostType1}}, 256: mongoose_subdomain_core:get_subdomain_info(Subdomain1)), 257: HostTypeExtra2 = #{host_type => ?DYNAMIC_HOST_TYPE1}, 258: HandlerWithHostType2 = mongoose_packet_handler:add_extra(Handler, HostTypeExtra2), 259: ?assertEqual({ok, #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern2, 260: parent_domain => no_parent_domain, subdomain => <<"some.fqdn">>, 261: packet_handler => HandlerWithHostType2}}, 262: mongoose_subdomain_core:get_subdomain_info(<<"some.fqdn">>)), 263: HostTypeExtra3 = #{host_type => ?DYNAMIC_HOST_TYPE2}, 264: HandlerWithHostType3 = mongoose_packet_handler:add_extra(Handler, HostTypeExtra3), 265: ?assertEqual({ok, #{host_type => ?DYNAMIC_HOST_TYPE2, subdomain_pattern => Pattern1, 266: parent_domain => hd(?DYNAMIC_DOMAINS), subdomain => Subdomain2, 267: packet_handler => HandlerWithHostType3}}, 268: mongoose_subdomain_core:get_subdomain_info(Subdomain2)), 269: ?assertEqual({error, not_found}, 270: mongoose_subdomain_core:get_subdomain_info(<<"unknown.subdomain">>)), 271: ok. 272: 273: handles_domain_removal_during_subdomain_registration(_Config) -> 274: %% NumOfDomains is just some big non-round number to ensure that more than 2 ets 275: %% selections are done during the call to mongoose_domain_core:for_each_domain/2. 276: %% currently max selection size is 100 domains. 277: NumOfDomains = 1234, 278: NumOfDomainsToRemove = 1234 div 4, 279: NewDomains = [<<"dummy_domain_", (integer_to_binary(N))/binary, ".localhost">> 280: || N <- lists:seq(1, NumOfDomains)], 281: [mongoose_domain_core:insert(Domain, ?DYNAMIC_HOST_TYPE1, dummy_src) 282: || Domain <- NewDomains], 283: meck:new(mongoose_domain_core, [passthrough]), 284: WrapperFn = make_wrapper_fn(NumOfDomainsToRemove * 2, NumOfDomainsToRemove), 285: meck:expect(mongoose_domain_core, for_each_domain, 286: fun(HostType, Fn) -> 287: meck:passthrough([HostType, WrapperFn(Fn)]) 288: end), 289: Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), 290: Handler = mongoose_packet_handler:new(?MODULE), 291: %% Note that mongoose_domain_core:for_each_domain/2 is used to register subdomain. 292: %% some domains are removed during subdomain registration, see make_wrapper_fn/2 293: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 294: Pattern1, Handler)), 295: mongoose_subdomain_core:sync(), 296: %% try to add some domains second time, as this is also possible during 297: %% subdomain registration 298: AllDomains = mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1), 299: [RegisteredDomain1, RegisteredDomain2 | _] = AllDomains, 300: mongoose_subdomain_core:add_domain(?DYNAMIC_HOST_TYPE1, RegisteredDomain1), 301: mongoose_subdomain_core:add_domain(?DYNAMIC_HOST_TYPE1, RegisteredDomain2), 302: %% and finally try to remove some domains second time 303: RemovedDomains = NewDomains -- AllDomains, 304: [RemovedDomain1, RemovedDomain2 | _] = RemovedDomains, 305: mongoose_subdomain_core:remove_domain(?DYNAMIC_HOST_TYPE1, RemovedDomain1), 306: mongoose_subdomain_core:remove_domain(?DYNAMIC_HOST_TYPE1, RemovedDomain2), 307: Subdomains = get_all_subdomains(), 308: ?assertEqual(NumOfDomains - NumOfDomainsToRemove, length(Subdomains)), 309: AllExpectedSubDomains = [mongoose_subdomain_utils:get_fqdn(Pattern1, Domain) 310: || Domain <- AllDomains], 311: ?assertEqualLists(AllExpectedSubDomains, Subdomains), 312: ?assertEqual(NumOfDomainsToRemove, 313: meck:num_calls(mongoose_lazy_routing, maybe_remove_subdomain, 1)), 314: RemovedSubdomains = [mongoose_subdomain_utils:get_fqdn(Pattern1, Domain) 315: || Domain <- RemovedDomains], 316: ?assertEqualLists(RemovedSubdomains, get_list_of_disabled_subdomains()), 317: no_collisions(), 318: meck:unload(mongoose_domain_core). 319: 320: prevents_double_subdomain_registration(_Config) -> 321: Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), 322: Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.fqdn"), 323: Handler = mongoose_packet_handler:new(?MODULE), 324: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 325: Pattern1, Handler)), 326: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 327: Pattern2, Handler)), 328: ?assertEqual({error, already_registered}, 329: mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 330: Pattern1, Handler)), 331: ?assertEqual({error, already_registered}, 332: mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 333: Pattern2, Handler)). 334: 335: %%------------------------------------------------------------------------------------- 336: %% test cases for subdomain names collisions. 337: %%------------------------------------------------------------------------------------- 338: %% There are three possible subdomain names collisions: 339: %% 1) Different domain/subdomain_pattern pairs produce one and the same subdomain. 340: %% 2) Attempt to register the same FQDN subdomain for 2 different host types. 341: %% 3) Domain/subdomain_pattern pair produces the same subdomain name as another 342: %% FQDN subdomain. 343: %% 344: %% Collisions of the first type can eliminated by allowing only one level subdomains, 345: %% e.g. ensuring that subdomain template corresponds to this regex "^[^.]*\.@HOST@$". 346: %% 347: %% Collisions of the second type are less critical as they can be detected during 348: %% init phase - they result in {error, subdomain_already_exists} return code, so 349: %% modules can detect it and crash at ?MODULE:start/2. 350: %% 351: %% Third type is hard to resolve in automatic way. One of the options is to ensure 352: %% that FQDN subdomains don't start with the same "prefix" as subdomain patterns. 353: %% 354: %% It's good idea to create a metric for such collisions, so devops can set some 355: %% alarm and react on it. 356: %% 357: %% The current behaviour rejects insertion of the conflicting subdomain, the original 358: %% subdomain must remain unchanged 359: %%------------------------------------------------------------------------------------- 360: prevents_prefix_subdomain_overriding_by_prefix_subdomain(_Config) -> 361: Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("sub.@HOST@"), 362: Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("sub.domain.@HOST@"), 363: Handler = mongoose_packet_handler:new(?MODULE, #{host_type => dummy_type}), 364: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 365: Pattern1, Handler)), 366: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 367: Pattern2, Handler)), 368: %% one prefix subdomain conflicts with another prefix subdomain 369: mongoose_domain_core:insert(<<"test">>, ?DYNAMIC_HOST_TYPE1, dummy_src), 370: mongoose_domain_core:insert(<<"domain.test">>, ?DYNAMIC_HOST_TYPE1, dummy_src), 371: ?assertEqualLists( 372: [<<"sub.domain.domain.test">>, <<"sub.domain.test">>, <<"sub.test">>], 373: get_all_subdomains()), 374: ?assertEqualLists([<<"test">>, <<"domain.test">>], 375: mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)), 376: %% "test" domain is added first, so subdomain for this domain must remain unchanged 377: ExpectedSubdomainInfo = 378: #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern2, 379: parent_domain => <<"test">>, packet_handler => Handler, 380: subdomain => <<"sub.domain.test">>}, 381: ?assertEqual({ok, ExpectedSubdomainInfo}, 382: mongoose_subdomain_core:get_subdomain_info(<<"sub.domain.test">>)), 383: ?assertEqual([#{what => subdomains_collision, subdomain => <<"sub.domain.test">>}], 384: get_list_of_subdomain_collisions()), 385: no_domain_collisions(), 386: meck:reset(mongoose_subdomain_core), 387: %% check that removal of "domain.test" domain doesn't affect 388: %% "sub.domain.test" subdomain 389: mongoose_domain_core:delete(<<"domain.test">>), 390: ?assertEqual([<<"test">>], 391: mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)), 392: ?assertEqualLists([<<"sub.domain.test">>, <<"sub.test">>], get_all_subdomains()), 393: ?assertEqual({ok, ExpectedSubdomainInfo}, 394: mongoose_subdomain_core:get_subdomain_info(<<"sub.domain.test">>)), 395: ?assertEqual([<<"sub.domain.domain.test">>], get_list_of_disabled_subdomains()), 396: no_collisions(). 397: 398: prevents_fqdn_subdomain_overriding_by_prefix_subdomain(_Config) -> 399: Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), 400: Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.fqdn"), 401: Handler = mongoose_packet_handler:new(?MODULE, #{host_type => dummy_type}), 402: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 403: Pattern1, Handler)), 404: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 405: Pattern2, Handler)), 406: %% FQDN subdomain conflicts with prefix subdomain 407: mongoose_domain_core:insert(<<"fqdn">>, ?DYNAMIC_HOST_TYPE1, dummy_src), 408: ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()), 409: ?assertEqual([<<"fqdn">>], 410: mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)), 411: %% FQDN subdomain is added first, so it must remain unchanged 412: ExpectedSubdomainInfo = 413: #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern2, 414: parent_domain => no_parent_domain, packet_handler => Handler, 415: subdomain => <<"subdomain.fqdn">>}, 416: ?assertEqual({ok, ExpectedSubdomainInfo}, 417: mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)), 418: ?assertEqual([#{what => subdomains_collision, subdomain => <<"subdomain.fqdn">>}], 419: get_list_of_subdomain_collisions()), 420: no_domain_collisions(), 421: meck:reset(mongoose_subdomain_core), 422: %% check that removal of "fqdn" domain doesn't affect FQDN subdomain 423: mongoose_domain_core:delete(<<"fqdn">>), 424: ?assertEqual([], mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)), 425: ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()), 426: ?assertEqual({ok, ExpectedSubdomainInfo}, 427: mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)), 428: no_collisions(). 429: 430: prevents_fqdn_subdomain_overriding_by_fqdn_subdomain(_Config) -> 431: Pattern = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.fqdn"), 432: Handler = mongoose_packet_handler:new(?MODULE, #{host_type => dummy_type}), 433: %% FQDN subdomain conflicts with another FQDN subdomain 434: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 435: Pattern, Handler)), 436: ?assertEqual({error, subdomain_already_exists}, 437: mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE2, 438: Pattern, Handler)), 439: %% FQDN subdomain for ?DYNAMIC_HOST_TYPE1 is registered first, so it must 440: %% remain unchanged 441: ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()), 442: ExpectedSubdomainInfo = 443: #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern, 444: parent_domain => no_parent_domain, packet_handler => Handler, 445: subdomain => <<"subdomain.fqdn">>}, 446: ?assertEqual({ok, ExpectedSubdomainInfo}, 447: mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)), 448: ?assertEqual([#{what => subdomains_collision, subdomain => <<"subdomain.fqdn">>}], 449: get_list_of_subdomain_collisions()), 450: no_domain_collisions(), 451: meck:reset(mongoose_subdomain_core), 452: %% check that unregistering FQDN subdomain for ?DYNAMIC_HOST_TYPE2 doesn't 453: %% affect FQDN subdomain for ?DYNAMIC_HOST_TYPE1 454: ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE2, 455: Pattern)), 456: ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()), 457: ?assertEqual({ok, ExpectedSubdomainInfo}, 458: mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)), 459: no_collisions(). 460: 461: prevents_prefix_subdomain_overriding_by_fqdn_subdomain(_Config) -> 462: Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), 463: Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.fqdn"), 464: Handler = mongoose_packet_handler:new(?MODULE, #{host_type => dummy_type}), 465: %% FQDN subdomain conflicts with another FQDN subdomain 466: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 467: Pattern1, Handler)), 468: mongoose_domain_core:insert(<<"fqdn">>, ?DYNAMIC_HOST_TYPE1, dummy_src), 469: ?assertEqual({error, subdomain_already_exists}, 470: mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 471: Pattern2, Handler)), 472: %% FQDN subdomain for ?DYNAMIC_HOST_TYPE1 is registered first, so it must 473: %% remain unchanged 474: ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()), 475: ExpectedSubdomainInfo = 476: #{host_type => ?DYNAMIC_HOST_TYPE1, subdomain_pattern => Pattern1, 477: parent_domain => <<"fqdn">>, packet_handler => Handler, 478: subdomain => <<"subdomain.fqdn">>}, 479: ?assertEqual({ok, ExpectedSubdomainInfo}, 480: mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)), 481: ?assertEqual([#{what => subdomains_collision, subdomain => <<"subdomain.fqdn">>}], 482: get_list_of_subdomain_collisions()), 483: no_domain_collisions(), 484: meck:reset(mongoose_subdomain_core), 485: %% check that unregistering FQDN subdomain for ?DYNAMIC_HOST_TYPE2 doesn't 486: %% affect FQDN subdomain for ?DYNAMIC_HOST_TYPE1 487: ?assertEqual(ok, mongoose_subdomain_core:unregister_subdomain(?DYNAMIC_HOST_TYPE1, 488: Pattern2)), 489: ?assertEqual([<<"subdomain.fqdn">>], get_all_subdomains()), 490: ?assertEqual({ok, ExpectedSubdomainInfo}, 491: mongoose_subdomain_core:get_subdomain_info(<<"subdomain.fqdn">>)), 492: no_collisions(). 493: 494: 495: %%------------------------------------------------------------------------------------- 496: %% test cases for domain/subdomain names collisions. 497: %%------------------------------------------------------------------------------------- 498: %% There are two possible domain/subdomain names collisions: 499: %% 1) Domain/subdomain_pattern pair produces the same subdomain name as another 500: %% existing top level domain 501: %% 2) FQDN subdomain is the same as some registered top level domain 502: %% 503: %% The naive domain/subdomain registration rejection is probably a bad option: 504: %% * Domains and subdomains ETS tables are managed asynchronously, in addition to 505: %% that subdomains patterns registration is done async as well. This all leaves 506: %% room for various race conditions if we try to just make a verification and 507: %% prohibit domain/subdomain registration in case of any collisions. 508: %% * The only way to avoid such race conditions is to block all async. ETSs 509: %% editing during the validation process, but this can result in big delays 510: %% during the MIM initialisation phase. 511: %% * Also it's not clear how to interpret registration of the "prefix" based 512: %% subdomain patterns, should we block the registration of the whole pattern 513: %% or just only conflicting subdomains registration. Blocking of the whole 514: %% pattern requires generation and verification of all the subdomains (with 515: %% ETS blocking during that process), which depends on domains ETS size and 516: %% might take too long. 517: %% * And the last big issue with simple registration rejection approach, different 518: %% nodes in the cluster might have different registration sequence. So we may 519: %% end up in a situation when some nodes registered domain name as a subdomain, 520: %% while other nodes registered it as a top level domain. 521: %% 522: %% The better way is to prohibit registration of a top level domain if it is equal 523: %% to any of the FQDN subdomains or if beginning of domain name matches the prefix 524: %% of any subdomain template. In this case we don't need to verify subdomains at all, 525: %% verification of domain names against some limited number of subdomains patterns is 526: %% enough. And the only problem that we need to solve - mongooseim_domain_core must 527: %% be aware of all the subdomain patterns before it registers the first dynamic 528: %% domain. This would require minor configuration rework, e.g. tracking of subdomain 529: %% templates preprocessing (mongoose_subdomain_utils:make_subdomain_pattern/1 calls) 530: %% during TOML config parsing. 531: %% 532: %% It's good idea to create a metric for such collisions, so devops can set some 533: %% alarm and react on it. 534: %% 535: %% The current behaviour just ensures detection of the domain/subdomain names 536: %% collision, both (domain and subdomain) records remain unchanged in the 537: %% corresponding ETS tables 538: %%------------------------------------------------------------------------------------- 539: detects_domain_conflict_with_prefix_subdomain(_Config) -> 540: Pattern = mongoose_subdomain_utils:make_subdomain_pattern("subdomain.@HOST@"), 541: Handler = mongoose_packet_handler:new(?MODULE), 542: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 543: Pattern, Handler)), 544: mongoose_domain_core:insert(<<"test.net">>, ?DYNAMIC_HOST_TYPE1, dummy_src), 545: %% without this sync call "subdomain.example.net" collision can be detected 546: %% twice, one time by check_subdomain_name/1 function and then second time 547: %% by check_domain_name/2. 548: mongoose_subdomain_core:sync(), 549: mongoose_domain_core:insert(<<"subdomain.test.net">>, ?DYNAMIC_HOST_TYPE2, dummy_src), 550: mongoose_domain_core:insert(<<"subdomain.test.org">>, ?DYNAMIC_HOST_TYPE2, dummy_src), 551: mongoose_domain_core:insert(<<"test.org">>, ?DYNAMIC_HOST_TYPE1, dummy_src), 552: ?assertEqualLists([<<"subdomain.test.org">>, <<"subdomain.test.net">>], 553: get_all_subdomains()), 554: ?assertEqualLists( 555: [<<"subdomain.test.org">>, <<"subdomain.test.net">> | ?DYNAMIC_DOMAINS], 556: mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE2)), 557: no_subdomain_collisions(), 558: ?assertEqual( 559: [#{what => check_domain_name_failed, domain => <<"subdomain.test.net">>}, 560: #{what => check_subdomain_name_failed, subdomain => <<"subdomain.test.org">>}], 561: get_list_of_domain_collisions()), 562: ?assertEqual([<<"subdomain.test.net">>], get_list_of_disabled_subdomains()). 563: 564: detects_domain_conflict_with_fqdn_subdomain(_Config) -> 565: Pattern1 = mongoose_subdomain_utils:make_subdomain_pattern("some.fqdn"), 566: Pattern2 = mongoose_subdomain_utils:make_subdomain_pattern("another.fqdn"), 567: Handler = mongoose_packet_handler:new(?MODULE), 568: 569: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 570: Pattern1, Handler)), 571: mongoose_domain_core:insert(<<"some.fqdn">>, ?DYNAMIC_HOST_TYPE1, dummy_src), 572: mongoose_domain_core:insert(<<"another.fqdn">>, ?DYNAMIC_HOST_TYPE1, dummy_src), 573: ?assertEqual(ok, mongoose_subdomain_core:register_subdomain(?DYNAMIC_HOST_TYPE1, 574: Pattern2, Handler)), 575: ?assertEqualLists([<<"some.fqdn">>, <<"another.fqdn">>], get_all_subdomains()), 576: ?assertEqualLists([<<"some.fqdn">>, <<"another.fqdn">>], 577: mongoose_domain_core:get_domains_by_host_type(?DYNAMIC_HOST_TYPE1)), 578: no_subdomain_collisions(), 579: ?assertEqual( 580: [#{what => check_domain_name_failed, domain => <<"some.fqdn">>}, 581: #{what => check_subdomain_name_failed, subdomain => <<"another.fqdn">>}], 582: get_list_of_domain_collisions()), 583: ?assertEqual([<<"some.fqdn">>], get_list_of_disabled_subdomains()). 584: 585: %%------------------------------------------------------------------- 586: %% internal functions 587: %%------------------------------------------------------------------- 588: setup_meck(TestCase) -> 589: meck:new(mongoose_lazy_routing, [no_link]), 590: meck:new(mongoose_subdomain_core, [no_link, passthrough]), 591: meck:expect(mongoose_lazy_routing, maybe_remove_domain, fun(_, _) -> ok end), 592: RemoveSubdomainFn = 593: if 594: detects_domain_conflict_with_prefix_subdomain =:= TestCase; 595: detects_domain_conflict_with_fqdn_subdomain =:= TestCase -> 596: %% Subdomain should never overshadow top level domain name. 597: %% In case of conflict with domain name, we want to remove 598: %% subdomain routing and IQ handling, but keep ETS record 599: %% of that subdomain for troubleshooting. 600: fun(_) -> ?assertEqual(whereis(mongoose_subdomain_core), self()) end; 601: true -> 602: fun(SubdomainInfo) -> 603: %% For all other cases ensure that subdomain is removed from 604: %% the ETS table before mongoose_lazy_routing module notified 605: %% about it 606: Subdomain = maps:get(subdomain, SubdomainInfo), 607: ?assertEqual({error, not_found}, 608: mongoose_subdomain_core:get_host_type(Subdomain)), 609: ?assertEqual(whereis(mongoose_subdomain_core), self()) 610: end 611: end, 612: meck:expect(mongoose_lazy_routing, maybe_remove_subdomain, RemoveSubdomainFn). 613: 614: get_all_subdomains() -> 615: mongoose_subdomain_core:sync(), 616: get_subdomains(). 617: 618: get_subdomains() -> 619: %% mongoose_subdomain_core table is indexed by subdomain name field 620: KeyPos = ets:info(mongoose_subdomain_core, keypos), 621: [element(KeyPos, Item) || Item <- ets:tab2list(mongoose_subdomain_core)]. 622: 623: make_wrapper_fn(N, M) when N > M -> 624: %% the wrapper function generates a new loop processing function 625: %% that pauses after after processing N domains, removes M of the 626: %% already processed domains and resumes after that. 627: fun(Fn) -> 628: put(number_of_iterations, 0), 629: fun(HostType, DomainName) -> 630: NumberOfIterations = get(number_of_iterations), 631: if 632: NumberOfIterations =:= N -> remove_some_domains(M); 633: true -> ok 634: end, 635: put(number_of_iterations, NumberOfIterations + 1), 636: Fn(HostType, DomainName) 637: end 638: end. 639: 640: remove_some_domains(N) -> 641: AllSubdomains = get_subdomains(), 642: [begin 643: {ok, Info} = mongoose_subdomain_core:get_subdomain_info(Subdomain), 644: ParentDomain = maps:get(parent_domain, Info), 645: mongoose_domain_core:delete(ParentDomain) 646: end || Subdomain <- lists:sublist(AllSubdomains, N)]. 647: 648: no_collisions() -> 649: no_domain_collisions(), 650: no_subdomain_collisions(). 651: 652: no_domain_collisions() -> 653: Hist = meck:history(mongoose_subdomain_core), 654: Errors = [Call || {_P, {_M, log_error = _F, [From, _] = _A}, _R} = Call <- Hist, 655: From =:= check_subdomain_name orelse From =:= check_domain_name], 656: ?assertEqual([], Errors). 657: 658: get_list_of_domain_collisions() -> 659: Hist = meck:history(mongoose_subdomain_core), 660: [Error || {_Pid, {_Mod, log_error = _Func, [From, Error] = _Args}, _Result} <- Hist, 661: From =:= check_subdomain_name orelse From =:= check_domain_name]. 662: 663: no_subdomain_collisions() -> 664: Hist = meck:history(mongoose_subdomain_core), 665: Errors = [Call || {_P, {_M, log_error = _F, [From, _] = _A}, _R} = Call <- Hist, 666: From =:= report_subdomains_collision], 667: ?assertEqual([], Errors). 668: 669: get_list_of_subdomain_collisions() -> 670: Hist = meck:history(mongoose_subdomain_core), 671: [Error || {_Pid, {_Mod, log_error = _Func, [From, Error] = _Args}, _Result} <- Hist, 672: From =:= report_subdomains_collision]. 673: 674: get_list_of_disabled_subdomains() -> 675: History = meck:history(mongoose_lazy_routing), 676: [maps:get(subdomain, Info) 677: || {_Pid, {_Mod, Func, [Info] = _Args}, _Result} <- History, 678: Func =:= maybe_remove_subdomain].