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