Current section

Files

Jump to
erldns src zones erldns_zone_decoder.erl
Raw

src/zones/erldns_zone_decoder.erl

-module(erldns_zone_decoder).
-moduledoc false.
-include_lib("dns_erlang/include/dns.hrl").
-include_lib("kernel/include/logger.hrl").
-include_lib("erldns/include/erldns.hrl").
-include_lib("public_key/include/public_key.hrl").
-export([decode/2, decode_record/2, parse_keysets/1]).
-ifdef(TEST).
-export([json_record_to_erlang/1]).
-endif.
-spec decode(json:decode_value(), [erldns_zone_codec:decoder()]) -> erldns:zone().
decode(#{~"name" := Name, ~"records" := JsonRecords} = Zone, Decoders) ->
Sha = maps:get(~"sha", Zone, ~""),
JsonKeys = maps:get(~"keys", Zone, []),
Records = lists:map(fun(JsonRecord) -> decode_record(JsonRecord, Decoders) end, JsonRecords),
FilteredRecords = lists:filter(record_filter(), Records),
DistinctRecords = lists:usort(FilteredRecords),
erldns_zone_codec:build_zone(Name, Sha, DistinctRecords, parse_keysets(JsonKeys)).
-spec decode_record(#{binary() => json:decode_value()}, [erldns_zone_codec:decoder()]) ->
not_implemented | dns:rr().
decode_record(JsonRecord, Decoders) ->
maybe
true ?= apply_context_options(JsonRecord),
not_implemented ?= json_record_to_erlang(JsonRecord),
not_implemented ?= try_custom_decoders(JsonRecord, Decoders),
?LOG_WARNING(
#{what => unsupported_record, record => JsonRecord},
#{domain => [erldns, zones]}
),
not_implemented
else
false ->
not_implemented;
Value ->
Value
end.
-spec parse_keysets([json:decode_value()]) -> [erldns:keyset()].
parse_keysets([]) ->
[];
parse_keysets(JsonKeys) ->
parse_keysets(JsonKeys, []).
% RFC4034: §3.1.5. Signature Expiration and Inception Fields
% The Signature Expiration and Inception field values specify a date
% and time in the form of a 32-bit unsigned number of seconds elapsed
% since 1 January 1970 00:00:00 UTC, ignoring leap seconds, in network
% byte order.
parse_keysets([], Keys) ->
Keys;
parse_keysets([Key | Rest], Keys) ->
KeySet =
#keyset{
key_signing_key = to_crypto_key(maps:get(~"ksk", Key)),
key_signing_key_tag = maps:get(~"ksk_keytag", Key),
key_signing_alg = maps:get(~"ksk_alg", Key),
zone_signing_key = to_crypto_key(maps:get(~"zsk", Key)),
zone_signing_key_tag = maps:get(~"zsk_keytag", Key),
zone_signing_alg = maps:get(~"zsk_alg", Key),
inception = calendar:rfc3339_to_system_time(
binary_to_list(maps:get(~"inception", Key)), [{unit, second}]
),
valid_until = calendar:rfc3339_to_system_time(
binary_to_list(maps:get(~"until", Key)), [{unit, second}]
)
},
parse_keysets(Rest, [KeySet | Keys]).
to_crypto_key(KeyBin) ->
DecodedKey = public_key:pem_entry_decode(lists:last(public_key:pem_decode(KeyBin))),
extract_key(DecodedKey).
extract_key(#'RSAPrivateKey'{publicExponent = E, modulus = M, privateExponent = N}) ->
[E, M, N];
extract_key(#'ECPrivateKey'{privateKey = Key, parameters = {namedCurve, ?'secp256r1'}}) ->
Key;
extract_key(#'ECPrivateKey'{privateKey = Key, parameters = {namedCurve, ?'secp384r1'}}) ->
Key;
extract_key(#'ECPrivateKey'{privateKey = Key, parameters = {namedCurve, ?'id-Ed25519'}}) ->
Key;
extract_key(#'ECPrivateKey'{privateKey = Key, parameters = {namedCurve, ?'id-Ed448'}}) ->
Key.
record_filter() ->
fun(R) -> R =/= not_implemented end.
%% Determine if a record should be used in this name server's context.
%%
%% If the context is undefined then the record will always be used.
%%
%% If the context is a list and has at least one condition that passes
%% then it will be included in the zone
-spec apply_context_options(dynamic()) -> boolean().
apply_context_options(#{~"context" := Context}) ->
case application:get_env(erldns, zones, #{}) of
#{context_options := ContextOptions} when is_map(ContextOptions) ->
apply_context_match_empty_check(
maps:get(match_empty, ContextOptions, false), Context
) orelse
apply_context_list_check(
maps:get(allow, ContextOptions, []), Context
);
_ ->
true
end;
apply_context_options(#{}) ->
true.
-spec apply_context_list_check(list(), list()) -> boolean().
apply_context_list_check(ContextAllow, Context) ->
ContextSet = sets:from_list(Context, [{version, 2}]),
ContextAllowSet = sets:from_list(ContextAllow, [{version, 2}]),
0 =/= sets:size(sets:intersection(ContextAllowSet, ContextSet)).
-spec apply_context_match_empty_check(true | dynamic(), [dynamic()]) -> boolean().
apply_context_match_empty_check(true, []) ->
true;
apply_context_match_empty_check(_, _) ->
false.
try_custom_decoders(_, []) ->
not_implemented;
try_custom_decoders(Data, [Decoder | Rest]) ->
case Decoder(Data) of
not_implemented ->
try_custom_decoders(Data, Rest);
Record ->
Record
end.
% Internal converters
-spec json_record_to_erlang(dynamic()) -> not_implemented | dns:rr().
json_record_to_erlang(#{~"data" := null} = Record) ->
?LOG_WARNING(#{what => error_parsing_record, record => Record}, #{domain => [erldns, zones]}),
not_implemented;
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"SOA", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_SOA,
data =
#dns_rrdata_soa{
mname = maps:get(~"mname", Data),
rname = maps:get(~"rname", Data),
serial = maps:get(~"serial", Data),
refresh = maps:get(~"refresh", Data),
retry = maps:get(~"retry", Data),
expire = maps:get(~"expire", Data),
minimum = maps:get(~"minimum", Data)
},
ttl = Ttl
};
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"NS", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_NS,
data = #dns_rrdata_ns{dname = maps:get(~"dname", Data)},
ttl = Ttl
};
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"A", ~"ttl" := Ttl, ~"data" := Data}) ->
case inet_parse:address(binary_to_list(maps:get(~"ip", Data))) of
{ok, Address} ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_A,
data = #dns_rrdata_a{ip = Address},
ttl = Ttl
};
{error, Reason} ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => ~"A",
data => Data,
reason => Reason
},
#{domain => [erldns, zones]}
),
not_implemented
end;
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"AAAA", ~"ttl" := Ttl, ~"data" := Data}) ->
case inet_parse:address(binary_to_list(maps:get(~"ip", Data))) of
{ok, Address} ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_AAAA,
data = #dns_rrdata_aaaa{ip = Address},
ttl = Ttl
};
{error, Reason} ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => ~"AAAA",
data => Data,
reason => Reason
},
#{domain => [erldns, zones]}
),
not_implemented
end;
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"CAA", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_CAA,
data =
#dns_rrdata_caa{
flags = maps:get(~"flags", Data),
tag = maps:get(~"tag", Data),
value = maps:get(~"value", Data)
},
ttl = Ttl
};
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"CNAME", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_CNAME,
data = #dns_rrdata_cname{dname = maps:get(~"dname", Data)},
ttl = Ttl
};
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"MX", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_MX,
data = #dns_rrdata_mx{
exchange = maps:get(~"exchange", Data), preference = maps:get(~"preference", Data)
},
ttl = Ttl
};
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"HINFO", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_HINFO,
data = #dns_rrdata_hinfo{cpu = maps:get(~"cpu", Data), os = maps:get(~"os", Data)},
ttl = Ttl
};
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"RP", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_RP,
data = #dns_rrdata_rp{mbox = maps:get(~"mbox", Data), txt = maps:get(~"txt", Data)},
ttl = Ttl
};
json_record_to_erlang(#{
~"name" := Name, ~"type" := ~"TXT", ~"ttl" := Ttl, ~"data" := #{~"txts" := Txts}
}) when is_list(Txts) ->
#dns_rr{name = Name, type = ?DNS_TYPE_TXT, data = #dns_rrdata_txt{txt = Txts}, ttl = Ttl};
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"PTR", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_PTR,
data = #dns_rrdata_ptr{dname = maps:get(~"dname", Data)},
ttl = Ttl
};
json_record_to_erlang(#{
~"name" := Name,
~"type" := Type = ~"SSHFP",
~"ttl" := Ttl,
~"data" := Data
}) ->
%% This function call may crash. Handle it as a bad record.
try binary:decode_hex(maps:get(~"fp", Data)) of
Fp ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_SSHFP,
data =
#dns_rrdata_sshfp{
alg = maps:get(~"alg", Data),
fp_type = maps:get(~"fptype", Data),
fp = Fp
},
ttl = Ttl
}
catch
Class:Reason ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
class => Class,
reason => Reason
},
#{domain => [erldns, zones]}
),
not_implemented
end;
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"SRV", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_SRV,
data =
#dns_rrdata_srv{
priority = maps:get(~"priority", Data),
weight = maps:get(~"weight", Data),
port = maps:get(~"port", Data),
target = maps:get(~"target", Data)
},
ttl = Ttl
};
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"NAPTR", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_NAPTR,
data =
#dns_rrdata_naptr{
order = maps:get(~"order", Data),
preference = maps:get(~"preference", Data),
flags = maps:get(~"flags", Data),
services = maps:get(~"services", Data),
regexp = maps:get(~"regexp", Data),
replacement = maps:get(~"replacement", Data)
},
ttl = Ttl
};
json_record_to_erlang(#{
~"name" := Name,
~"type" := Type = ~"DS",
~"ttl" := Ttl,
~"data" := Data
}) ->
try binary:decode_hex(maps:get(~"digest", Data)) of
Digest ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_DS,
data =
#dns_rrdata_ds{
keytag = maps:get(~"keytag", Data),
alg = maps:get(~"alg", Data),
digest_type = maps:get(~"digest_type", Data),
digest = Digest
},
ttl = Ttl
}
catch
Class:Reason ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
class => Class,
reason => Reason
},
#{domain => [erldns, zones]}
),
not_implemented
end;
json_record_to_erlang(#{
~"name" := Name,
~"type" := Type = ~"TLSA",
~"ttl" := Ttl,
~"data" := Data
}) ->
try binary:decode_hex(maps:get(~"certificate", Data)) of
Certificate ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_TLSA,
data =
#dns_rrdata_tlsa{
usage = maps:get(~"usage", Data),
selector = maps:get(~"selector", Data),
matching_type = maps:get(~"matching_type", Data),
certificate = Certificate
},
ttl = Ttl
}
catch
Class:Reason ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
class => Class,
reason => Reason
},
#{domain => [erldns, zones]}
),
not_implemented
end;
json_record_to_erlang(#{
~"name" := Name,
~"type" := Type = ~"CDS",
~"ttl" := Ttl,
~"data" := Data
}) ->
try binary:decode_hex(maps:get(~"digest", Data)) of
Digest ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_CDS,
data =
#dns_rrdata_cds{
keytag = maps:get(~"keytag", Data),
alg = maps:get(~"alg", Data),
digest_type = maps:get(~"digest_type", Data),
digest = Digest
},
ttl = Ttl
}
catch
Class:Reason ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
class => Class,
reason => Reason
},
#{domain => [erldns, zones]}
),
not_implemented
end;
json_record_to_erlang(#{
~"name" := Name, ~"type" := Type = ~"DNSKEY", ~"ttl" := Ttl, ~"data" := Data
}) ->
try base64:decode(maps:get(~"public_key", Data)) of
PublicKey ->
dnssec:add_keytag_to_dnskey(#dns_rr{
name = Name,
type = ?DNS_TYPE_DNSKEY,
data =
#dns_rrdata_dnskey{
flags = maps:get(~"flags", Data),
protocol = maps:get(~"protocol", Data),
alg = maps:get(~"alg", Data),
public_key = PublicKey,
keytag = 0
},
ttl = Ttl
})
catch
Class:Reason ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
class => Class,
reason => Reason
},
#{domain => [erldns, zones]}
),
not_implemented
end;
json_record_to_erlang(#{
~"name" := Name, ~"type" := Type = ~"CDNSKEY", ~"ttl" := Ttl, ~"data" := Data
}) ->
try base64:decode(maps:get(~"public_key", Data)) of
PublicKey ->
dnssec:add_keytag_to_cdnskey(#dns_rr{
name = Name,
type = ?DNS_TYPE_CDNSKEY,
data =
#dns_rrdata_cdnskey{
flags = maps:get(~"flags", Data),
protocol = maps:get(~"protocol", Data),
alg = maps:get(~"alg", Data),
public_key = PublicKey,
keytag = 0
},
ttl = Ttl
})
catch
Class:Reason ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
class => Class,
reason => Reason
},
#{domain => [erldns, zones]}
),
not_implemented
end;
json_record_to_erlang(#{}) ->
not_implemented.