fp-lab4/lib/exdns/http/codec/record.ex
2026-01-13 18:26:26 +03:00

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