-module(erldns_zone_loader_worker). -moduledoc false. -include_lib("dns_erlang/include/dns.hrl"). -export([load_file/4, start_link/4, init/4]). -ifdef(TEST). -export([do_load_file/3]). -endif. -spec load_file(erldns_zone_loader_getter:tag(), file:filename(), file:name(), boolean()) -> reference(). load_file(Tag, File, KeysPath, Strict) -> {ok, Pid} = erldns_zone_loader_worker_sup:start_child([Tag, File, KeysPath, Strict]), AliasMon = erlang:monitor(process, Pid, [{alias, reply_demonitor}]), Pid ! {?MODULE, AliasMon}, AliasMon. -spec start_link(erldns_zone_loader_getter:tag(), file:filename(), file:name(), boolean()) -> dynamic(). start_link(Tag, File, KeysPath, Strict) -> proc_lib:start_link(?MODULE, init, [Tag, File, KeysPath, Strict]). -spec init(erldns_zone_loader_getter:tag(), file:filename(), file:name(), boolean()) -> no_return(). init(Tag, File, KeysPath, Strict) -> proc_lib:init_ack({ok, self()}), receive {?MODULE, AliasMon} -> run(Tag, File, KeysPath, Strict, AliasMon) after 1000 -> exit(timeout) end. run(Tag, File, KeysPath, Strict, AliasMon) -> try Zones = do_load_file(File, KeysPath, Strict), lists:map(fun erldns_zone_cache:put_zone/1, Zones), AliasMon ! {Tag, AliasMon, length(Zones)} catch throw:Reason -> AliasMon ! {Tag, AliasMon, error, Reason}; error:unexpected_end = Error -> AliasMon ! {Tag, AliasMon, error, {json_error, #{error => Error, file => File}}}; error:{invalid_byte, _} = Error -> AliasMon ! {Tag, AliasMon, error, {json_error, #{error => Error, file => File}}}; error:{unexpected_sequence, _} = Error -> AliasMon ! {Tag, AliasMon, error, {json_error, #{error => Error, file => File}}}; error:Reason:StackTrace -> AliasMon ! {Tag, AliasMon, error, {Reason, StackTrace}} end. -spec do_load_file(file:filename(), file:name(), boolean()) -> [erldns:zone()]. do_load_file(File, KeysPath, Strict) -> case filename:extension(File) of ".json" -> load_json_file(File, Strict); ".zone" -> load_zone_file(File, KeysPath, Strict) end. -spec load_json_file(file:filename(), boolean()) -> [erldns:zone()]. load_json_file(File, Strict) -> maybe {ok, Content} ?= file:read_file(File, [raw]), {ok, List} ?= safe_json_decode_list(Content), Zones = ensure_zones(List, Strict), [erldns_zone_codec:decode(Zone) || Zone <- Zones] else [] -> []; {json_error, Reason} -> Strict andalso erlang:throw({invalid_zone_file, Reason}), []; {error, Reason} -> Strict andalso erlang:throw({file_read_error, File, Reason}), [] end. -spec ensure_zones([json:decode_value()], boolean()) -> [json:decode_value()] | no_return(). ensure_zones([#{~"name" := _, ~"records" := _} = H | T], Strict) -> [H | ensure_zones(T, Strict)]; ensure_zones([_ | T], Strict) -> Strict andalso erlang:throw(invalid_zone_file), ensure_zones(T, Strict); ensure_zones([], _) -> []. -spec load_zone_file(file:filename(), file:name(), boolean()) -> [erldns:zone()]. load_zone_file(File, KeysPath, Strict) -> maybe {ok, Records} ?= dns_zone:parse_file(File), Soa = #dns_rr{name = ZoneName} ?= lists:keyfind(?DNS_TYPE_SOA, #dns_rr.type, Records), Sha = crypto:hash(sha256, term_to_binary(Soa)), Keys = load_private_keys(ZoneName, KeysPath), [erldns_zone_codec:build_zone(ZoneName, Sha, Records, Keys)] else false -> Strict andalso erlang:throw({invalid_zone_file, File}), []; {error, Reason} -> Strict andalso erlang:throw({file_read_error, File, Reason}), [] end. %% Finds and loads private keys for every DNSKEY record in the list. -spec load_private_keys(dns:dname(), file:name()) -> [erldns:keyset()]. load_private_keys(ZoneName, KeysDir) -> Trimmed = string:trim(ZoneName, trailing, "."), FileName = <>, FullPath = filename:join(KeysDir, FileName), maybe {ok, Content} ?= file:read_file(FullPath, [raw]), {ok, Json} ?= safe_json_decode_list(Content), erldns_zone_decoder:parse_keysets(Json) else {json_error, Reason} -> erlang:throw({invalid_key_file, FullPath, Reason}); {error, _} -> %% File not found - This is normal for public keys we don't own %% (or if we only have the ZSK but the KSK is held offline) [] end. -spec safe_json_decode_list(binary()) -> {ok, [json:decode_value()]} | {json_error, atom()}. safe_json_decode_list(Binary) -> case json:decode(Binary) of List when is_list(List) -> {ok, List}; _ -> {json_error, invalid_zone_file} end.