Packages
erldns
9.1.0
11.0.2
11.0.1
11.0.0
10.6.0
10.5.6
10.5.5
10.5.4
10.5.3
10.5.2
10.5.1
10.5.0
10.4.4
10.4.3
10.4.2
10.4.1
10.4.0
10.3.0
10.2.1
10.2.0
10.1.0
10.0.0
10.0.0-rc4
10.0.0-rc3
10.0.0-rc2
10.0.0-rc1
9.1.0
9.0.0
9.0.0-rc3
9.0.0-rc2
9.0.0-rc1
8.1.0
8.0.0
8.0.0-rc6
8.0.0-rc5
8.0.0-rc4
8.0.0-rc3
8.0.0-rc2
8.0.0-rc1
7.0.0
7.0.0-rc9
7.0.0-rc8
7.0.0-rc7
7.0.0-rc6
7.0.0-rc5
7.0.0-rc4
7.0.0-rc3
7.0.0-rc2
7.0.0-rc12
7.0.0-rc11
7.0.0-rc10
7.0.0-rc1
6.0.2
6.0.1
6.0.0
5.0.0
4.3.1
4.3.0
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.2
4.1.1
4.1.0
4.0.0
3.0.0
1.0.0
Erlang Authoritative DNS Server
Current section
Files
Jump to
Current section
Files
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.