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