205 lines
6.4 KiB
Elixir
205 lines
6.4 KiB
Elixir
defmodule Exdns.HttpCodec.Record do
|
|
@moduledoc false
|
|
|
|
alias Exdns.DnsPacket.ResourceRecord
|
|
alias Exdns.DnsPacket.RRData.Types
|
|
alias Exdns.DnsPacket.RRType
|
|
alias Exdns.HttpCodec
|
|
|
|
@type_map %{
|
|
"a" => :a,
|
|
"aaaa" => :aaaa,
|
|
"cname" => :cname,
|
|
"txt" => :txt,
|
|
"mx" => :mx,
|
|
"srv" => :srv,
|
|
"caa" => :caa,
|
|
"soa" => :soa,
|
|
"ns" => :ns
|
|
}
|
|
|
|
def decode(%{} = record, zone_name) do
|
|
with {:ok, type} <- decode_type(record),
|
|
{:ok, name} <- decode_name(Map.get(record, "name"), zone_name),
|
|
{:ok, rdata} <- decode_rdata(type, Map.get(record, "rdata", %{})) do
|
|
{:ok,
|
|
%ResourceRecord{
|
|
name: name,
|
|
type: type,
|
|
class: Map.get(record, "class", 1),
|
|
ttl: Map.get(record, "ttl", 0),
|
|
rdata: rdata
|
|
}}
|
|
end
|
|
end
|
|
|
|
def decode(_, _zone_name), do: {:error, "invalid_record"}
|
|
|
|
def encode(%ResourceRecord{} = rr) do
|
|
%{
|
|
"name" => HttpCodec.name_to_string(rr.name),
|
|
"type" => encode_type(rr),
|
|
"class" => rr.class,
|
|
"ttl" => rr.ttl,
|
|
"rdata" => encode_rdata(rr.rdata)
|
|
}
|
|
end
|
|
|
|
defp decode_type(%{"type" => type}), do: decode_type_value(type)
|
|
defp decode_type(%{type: type}), do: decode_type_value(type)
|
|
defp decode_type(_), do: {:error, "missing_type"}
|
|
|
|
defp decode_type_value(type) when is_binary(type) do
|
|
case Map.fetch(@type_map, String.downcase(type)) do
|
|
{:ok, mapped} -> {:ok, mapped}
|
|
:error -> {:error, "unsupported_type"}
|
|
end
|
|
end
|
|
|
|
defp decode_type_value(type) when is_atom(type), do: {:ok, type}
|
|
defp decode_type_value(type) when is_integer(type), do: {:ok, RRType.from_code(type)}
|
|
defp decode_type_value(_), do: {:error, "invalid_type"}
|
|
|
|
defp decode_name(nil, zone_name), do: {:ok, zone_name}
|
|
defp decode_name("@", zone_name), do: {:ok, zone_name}
|
|
defp decode_name("*", zone_name), do: {:ok, ["*" | zone_name]}
|
|
|
|
defp decode_name(name, _zone_name) when is_binary(name) do
|
|
{:ok, HttpCodec.name_from_string(name)}
|
|
end
|
|
|
|
defp decode_name(["@"], zone_name), do: {:ok, zone_name}
|
|
defp decode_name(["*"], zone_name), do: {:ok, ["*" | zone_name]}
|
|
defp decode_name(name, _zone_name) when is_list(name), do: {:ok, name}
|
|
defp decode_name(_, _zone_name), do: {:error, "invalid_name"}
|
|
|
|
defp decode_rdata(:a, %{"address" => address}), do: decode_a(address)
|
|
defp decode_rdata(:aaaa, %{"address" => address}), do: decode_aaaa(address)
|
|
|
|
defp decode_rdata(:cname, %{"name" => name}),
|
|
do: {:ok, %Types.CNAME{name: HttpCodec.name_from_string(name)}}
|
|
|
|
defp decode_rdata(:ns, %{"name" => name}),
|
|
do: {:ok, %Types.NS{name: HttpCodec.name_from_string(name)}}
|
|
|
|
defp decode_rdata(:txt, %{"strings" => strings}),
|
|
do: {:ok, %Types.TXT{strings: wrap_list(strings)}}
|
|
|
|
defp decode_rdata(:mx, %{"preference" => pref, "exchange" => exch}),
|
|
do: {:ok, %Types.MX{preference: pref, exchange: HttpCodec.name_from_string(exch)}}
|
|
|
|
defp decode_rdata(:srv, %{
|
|
"priority" => prio,
|
|
"weight" => weight,
|
|
"port" => port,
|
|
"target" => target
|
|
}),
|
|
do:
|
|
{:ok,
|
|
%Types.SRV{
|
|
priority: prio,
|
|
weight: weight,
|
|
port: port,
|
|
target: HttpCodec.name_from_string(target)
|
|
}}
|
|
|
|
defp decode_rdata(:caa, %{"flags" => flags, "tag" => tag, "value" => value}),
|
|
do: {:ok, %Types.CAA{flags: flags, tag: tag, value: value}}
|
|
|
|
defp decode_rdata(
|
|
:soa,
|
|
%{
|
|
"mname" => mname,
|
|
"rname" => rname,
|
|
"serial" => serial,
|
|
"refresh" => refresh,
|
|
"retry" => retry,
|
|
"expire" => expire,
|
|
"minimum" => minimum
|
|
}
|
|
) do
|
|
{:ok,
|
|
%Types.SOA{
|
|
mname: HttpCodec.name_from_string(mname),
|
|
rname: HttpCodec.name_from_string(rname),
|
|
serial: serial,
|
|
refresh: refresh,
|
|
retry: retry,
|
|
expire: expire,
|
|
minimum: minimum
|
|
}}
|
|
end
|
|
|
|
defp decode_rdata(_, _), do: {:error, "invalid_rdata"}
|
|
|
|
defp decode_a(address) when is_binary(address) do
|
|
case :inet.parse_address(to_charlist(address)) do
|
|
{:ok, {_, _, _, _} = tuple} -> {:ok, %Types.A{address: tuple}}
|
|
_ -> {:error, "invalid_ipv4"}
|
|
end
|
|
end
|
|
|
|
defp decode_a([a, b, c, d]), do: {:ok, %Types.A{address: {a, b, c, d}}}
|
|
defp decode_a(_), do: {:error, "invalid_ipv4"}
|
|
|
|
defp decode_aaaa(address) when is_binary(address) do
|
|
case :inet.parse_address(to_charlist(address)) do
|
|
{:ok, {_, _, _, _, _, _, _, _} = tuple} -> {:ok, %Types.AAAA{address: tuple}}
|
|
_ -> {:error, "invalid_ipv6"}
|
|
end
|
|
end
|
|
|
|
defp decode_aaaa([a, b, c, d, e, f, g, h]),
|
|
do: {:ok, %Types.AAAA{address: {a, b, c, d, e, f, g, h}}}
|
|
|
|
defp decode_aaaa(_), do: {:error, "invalid_ipv6"}
|
|
|
|
defp encode_type(%ResourceRecord{type: type}) when is_atom(type), do: Atom.to_string(type)
|
|
defp encode_type(%ResourceRecord{type: type}) when is_integer(type), do: type
|
|
|
|
defp encode_rdata(%Types.A{address: {a, b, c, d}}), do: %{"address" => "#{a}.#{b}.#{c}.#{d}"}
|
|
defp encode_rdata(%Types.AAAA{address: addr}), do: %{"address" => ip_to_string(addr)}
|
|
defp encode_rdata(%Types.CNAME{name: name}), do: %{"name" => HttpCodec.name_to_string(name)}
|
|
defp encode_rdata(%Types.NS{name: name}), do: %{"name" => HttpCodec.name_to_string(name)}
|
|
defp encode_rdata(%Types.TXT{strings: strings}), do: %{"strings" => strings}
|
|
|
|
defp encode_rdata(%Types.MX{preference: pref, exchange: exchange}),
|
|
do: %{"preference" => pref, "exchange" => HttpCodec.name_to_string(exchange)}
|
|
|
|
defp encode_rdata(%Types.SRV{priority: prio, weight: weight, port: port, target: target}) do
|
|
%{
|
|
"priority" => prio,
|
|
"weight" => weight,
|
|
"port" => port,
|
|
"target" => HttpCodec.name_to_string(target)
|
|
}
|
|
end
|
|
|
|
defp encode_rdata(%Types.CAA{flags: flags, tag: tag, value: value}),
|
|
do: %{"flags" => flags, "tag" => tag, "value" => value}
|
|
|
|
defp encode_rdata(%Types.SOA{} = soa) do
|
|
%{
|
|
"mname" => HttpCodec.name_to_string(soa.mname),
|
|
"rname" => HttpCodec.name_to_string(soa.rname),
|
|
"serial" => soa.serial,
|
|
"refresh" => soa.refresh,
|
|
"retry" => soa.retry,
|
|
"expire" => soa.expire,
|
|
"minimum" => soa.minimum
|
|
}
|
|
end
|
|
|
|
defp encode_rdata(bin) when is_binary(bin), do: %{"raw" => Base.encode64(bin)}
|
|
defp encode_rdata(_), do: %{}
|
|
|
|
defp wrap_list(list) when is_list(list), do: list
|
|
defp wrap_list(item), do: [item]
|
|
|
|
defp ip_to_string(tuple) do
|
|
tuple
|
|
|> Tuple.to_list()
|
|
|> Enum.map_join(":", &Integer.to_string/1)
|
|
end
|
|
end
|