Packages
erldns
10.0.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").
-define(LOG_METADATA, #{domain => [erldns, zones, decoder]}).
-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}, ?LOG_METADATA),
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}, ?LOG_METADATA),
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
},
?LOG_METADATA
),
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
},
?LOG_METADATA
),
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
},
?LOG_METADATA
),
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
},
?LOG_METADATA
),
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
},
?LOG_METADATA
),
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
},
?LOG_METADATA
),
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
},
?LOG_METADATA
),
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
},
?LOG_METADATA
),
not_implemented
end;
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"SVCB", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_SVCB,
data =
#dns_rrdata_svcb{
svc_priority = maps:get(~"svc_priority", Data),
target_name = maps:get(~"target_name", Data),
svc_params = parse_svcb_params(maps:get(~"svc_params", Data, #{}))
},
ttl = Ttl
};
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"HTTPS", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_HTTPS,
data =
#dns_rrdata_https{
svc_priority = maps:get(~"svc_priority", Data),
target_name = maps:get(~"target_name", Data),
svc_params = parse_svcb_params(maps:get(~"svc_params", Data, #{}))
},
ttl = Ttl
};
json_record_to_erlang(#{
~"name" := Name,
~"type" := Type = ~"OPENPGPKEY",
~"ttl" := Ttl,
~"data" := Data
}) ->
try base64:decode(maps:get(~"data", Data)) of
PgpData ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_OPENPGPKEY,
data = #dns_rrdata_openpgpkey{data = PgpData},
ttl = Ttl
}
catch
Class:Reason ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
class => Class,
reason => Reason
},
?LOG_METADATA
),
not_implemented
end;
json_record_to_erlang(#{
~"name" := Name,
~"type" := Type = ~"SMIMEA",
~"ttl" := Ttl,
~"data" := Data
}) ->
try binary:decode_hex(maps:get(~"certificate", Data)) of
Certificate ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_SMIMEA,
data =
#dns_rrdata_smimea{
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
},
?LOG_METADATA
),
not_implemented
end;
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"URI", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_URI,
data =
#dns_rrdata_uri{
priority = maps:get(~"priority", Data),
weight = maps:get(~"weight", Data),
target = maps:get(~"target", Data)
},
ttl = Ttl
};
json_record_to_erlang(#{
~"name" := Name,
~"type" := Type = ~"WALLET",
~"ttl" := Ttl,
~"data" := Data
}) ->
try base64:decode(maps:get(~"data", Data)) of
WalletData ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_WALLET,
data = #dns_rrdata_wallet{data = WalletData},
ttl = Ttl
}
catch
Class:Reason ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
class => Class,
reason => Reason
},
?LOG_METADATA
),
not_implemented
end;
json_record_to_erlang(#{
~"name" := Name,
~"type" := Type = ~"EUI48",
~"ttl" := Ttl,
~"data" := Data
}) ->
try
AddressHex = maps:get(~"address", Data),
AddressBin = binary:decode_hex(AddressHex),
case byte_size(AddressBin) of
6 ->
<<Address:48>> = AddressBin,
#dns_rr{
name = Name,
type = ?DNS_TYPE_EUI48,
data = #dns_rrdata_eui48{address = <<Address:48>>},
ttl = Ttl
};
_ ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
reason => invalid_eui48_length
},
?LOG_METADATA
),
not_implemented
end
catch
Class:Reason ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
class => Class,
reason => Reason
},
?LOG_METADATA
),
not_implemented
end;
json_record_to_erlang(#{
~"name" := Name,
~"type" := Type = ~"EUI64",
~"ttl" := Ttl,
~"data" := Data
}) ->
try
AddressHex = maps:get(~"address", Data),
AddressBin = binary:decode_hex(AddressHex),
case byte_size(AddressBin) of
8 ->
<<Address:64>> = AddressBin,
#dns_rr{
name = Name,
type = ?DNS_TYPE_EUI64,
data = #dns_rrdata_eui64{address = <<Address:64>>},
ttl = Ttl
};
_ ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
reason => invalid_eui64_length
},
?LOG_METADATA
),
not_implemented
end
catch
Class:Reason ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
class => Class,
reason => Reason
},
?LOG_METADATA
),
not_implemented
end;
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"CSYNC", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_CSYNC,
data =
#dns_rrdata_csync{
soa_serial = maps:get(~"soa_serial", Data),
flags = maps:get(~"flags", Data),
types = maps:get(~"types", Data, [])
},
ttl = Ttl
};
json_record_to_erlang(#{~"name" := Name, ~"type" := ~"DSYNC", ~"ttl" := Ttl, ~"data" := Data}) ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_DSYNC,
data =
#dns_rrdata_dsync{
rrtype = maps:get(~"rrtype", Data),
scheme = maps:get(~"scheme", Data),
port = maps:get(~"port", Data),
target = maps:get(~"target", Data)
},
ttl = Ttl
};
json_record_to_erlang(#{
~"name" := Name,
~"type" := Type = ~"ZONEMD",
~"ttl" := Ttl,
~"data" := Data
}) ->
try binary:decode_hex(maps:get(~"hash", Data)) of
Hash ->
#dns_rr{
name = Name,
type = ?DNS_TYPE_ZONEMD,
data =
#dns_rrdata_zonemd{
serial = maps:get(~"serial", Data),
scheme = maps:get(~"scheme", Data),
algorithm = maps:get(~"algorithm", Data),
hash = Hash
},
ttl = Ttl
}
catch
Class:Reason ->
?LOG_ERROR(
#{
what => error_parsing_record,
name => Name,
type => Type,
data => Data,
class => Class,
reason => Reason
},
?LOG_METADATA
),
not_implemented
end;
json_record_to_erlang(#{}) ->
not_implemented.
%% Helper function to parse SVCB service parameters from JSON
-spec parse_svcb_params(map()) -> dns:svcb_svc_params().
parse_svcb_params(Params) when is_map(Params) ->
SvcParams = parse_svcb_params(maps:to_list(Params), #{}),
validate_mandatory_params(SvcParams).
-spec parse_svcb_params([{binary(), term()}], dns:svcb_svc_params()) -> dns:svcb_svc_params().
parse_svcb_params([], Acc) ->
Acc;
parse_svcb_params([{Key, Value} | Rest], Acc) ->
ParamKey = dns_names:name_svcb_param(Key),
NewAcc =
case ParamKey of
undefined ->
%% invalid parameter key, skip it. Note that dns_erlang parses keyNNNN here already
Acc;
?DNS_SVCB_PARAM_MANDATORY when is_list(Value) ->
KeyNums = [dns_names:name_svcb_param(K) || K <- Value],
Acc#{ParamKey => KeyNums};
?DNS_SVCB_PARAM_ALPN when is_list(Value) ->
Acc#{ParamKey => Value};
?DNS_SVCB_PARAM_NO_DEFAULT_ALPN when Value =:= null ->
Acc#{ParamKey => none};
?DNS_SVCB_PARAM_PORT when is_integer(Value) ->
Acc#{ParamKey => Value};
?DNS_SVCB_PARAM_ECH when is_binary(Value) ->
Acc#{ParamKey => Value};
?DNS_SVCB_PARAM_IPV4HINT when is_list(Value) ->
IPs = [parse_ipv4(IP) || IP <- Value],
Acc#{ParamKey => IPs};
?DNS_SVCB_PARAM_IPV6HINT when is_list(Value) ->
IPs = [parse_ipv6(IP) || IP <- Value],
Acc#{ParamKey => IPs};
NNNN when is_integer(NNNN) ->
Acc#{NNNN => Value}
end,
parse_svcb_params(Rest, NewAcc).
-spec parse_ipv4(binary() | string()) -> inet:ip4_address().
parse_ipv4(IP) when is_binary(IP) ->
parse_ipv4(binary_to_list(IP));
parse_ipv4(IP) when is_list(IP) ->
case inet:parse_address(IP) of
{ok, {A, B, C, D}} -> {A, B, C, D};
_ -> throw({invalid_ipv4, IP})
end.
-spec parse_ipv6(binary() | string()) -> inet:ip6_address().
parse_ipv6(IP) when is_binary(IP) ->
parse_ipv6(binary_to_list(IP));
parse_ipv6(IP) when is_list(IP) ->
case inet:parse_address(IP) of
{ok, {A, B, C, D, E, F, G, H}} -> {A, B, C, D, E, F, G, H};
_ -> throw({invalid_ipv6, IP})
end.
%% Validate mandatory parameter self-consistency
%% RFC 9460: Keys listed in mandatory parameter must exist in SvcParams
%% and mandatory (key 0) cannot reference itself
-spec validate_mandatory_params(dns:svcb_svc_params()) -> dns:svcb_svc_params() | no_return().
validate_mandatory_params(#{?DNS_SVCB_PARAM_MANDATORY := MandatoryKeys} = SvcParams) ->
%% Check that mandatory doesn't reference itself (key 0)
case lists:member(?DNS_SVCB_PARAM_MANDATORY, MandatoryKeys) of
true ->
Reason = {mandatory_self_reference, ?DNS_SVCB_PARAM_MANDATORY},
throw({svcb_mandatory_validation_error, Reason});
false ->
%% Check that all mandatory keys exist in SvcParams
MissingKeys = [K || K <- MandatoryKeys, not maps:is_key(K, SvcParams)],
case MissingKeys of
[] ->
SvcParams;
_ ->
Reason = {missing_mandatory_keys, MissingKeys},
throw({svcb_mandatory_validation_error, Reason})
end
end;
validate_mandatory_params(SvcParams) ->
SvcParams.