fp-lab4/lib/exdns/zone/rules.ex
2026-01-13 18:26:26 +03:00

148 lines
4.6 KiB
Elixir

defmodule Exdns.ZoneRules do
@moduledoc false
alias Exdns.DnsPacket.{ResourceRecord, RRType}
alias Exdns.DnsPacket.RRData.Types
def normalize_records(name, records, existing_records) do
serial = next_serial(existing_records)
records
|> ensure_soa(name, serial)
|> ensure_ns(name)
end
def find_authority(zone, labels) do
labels
|> suffixes()
|> Enum.reduce_while(:error, fn name, _ ->
records = Map.get(zone, name, [])
soa = filter_records(records, :soa)
ns = filter_records(records, :ns)
if soa != [] or ns != [] do
{:halt, {:ok, %{name: name, soa: soa, ns: ns}}}
else
{:cont, :error}
end
end)
end
def lookup_records(zone, name) do
name
|> suffixes()
|> Enum.reduce_while(:error, fn zone_name, _ -> lookup_in_zone(zone, name, zone_name) end)
end
def remove_records(records, to_remove) when is_list(records) and is_list(to_remove) do
remove_set = MapSet.new(to_remove)
Enum.reject(records, &MapSet.member?(remove_set, &1))
end
defp next_serial(records) do
case find_soa(records) do
%Types.SOA{serial: serial} when is_integer(serial) -> serial + 1
_ -> :os.system_time(:second)
end
end
defp ensure_soa(records, zone_name, serial) do
if Enum.any?(records, &soa_record?/1) do
Enum.map(records, fn
%ResourceRecord{rdata: %Types.SOA{} = soa} = rr ->
%ResourceRecord{rr | rdata: %Types.SOA{soa | serial: serial}}
rr ->
rr
end)
else
[default_soa(zone_name, serial) | records]
end
end
defp ensure_ns(records, zone_name) do
if Enum.any?(records, &ns_record?/1) do
records
else
[default_ns(zone_name) | records]
end
end
defp default_soa(zone_name, serial) do
%ResourceRecord{
name: zone_name,
type: :soa,
class: 1,
ttl: 3600,
rdata: %Types.SOA{
mname: default_ns_name(zone_name),
rname: default_rname(zone_name),
serial: serial,
refresh: 3600,
retry: 600,
expire: 1_209_600,
minimum: 300
}
}
end
defp default_ns(zone_name) do
%ResourceRecord{
name: zone_name,
type: :ns,
class: 1,
ttl: 3600,
rdata: %Types.NS{name: default_ns_name(zone_name)}
}
end
defp default_ns_name(zone_name), do: ["ns1" | zone_name]
defp default_rname(zone_name), do: ["hostmaster" | zone_name]
defp soa_record?(%ResourceRecord{rdata: %Types.SOA{}}), do: true
defp soa_record?(%ResourceRecord{} = rr), do: rr_type_atom(rr) == :soa
defp soa_record?(_), do: false
defp ns_record?(%ResourceRecord{rdata: %Types.NS{}}), do: true
defp ns_record?(%ResourceRecord{} = rr), do: rr_type_atom(rr) == :ns
defp ns_record?(_), do: false
defp find_soa(records) do
records
|> Enum.find(&soa_record?/1)
|> case do
%ResourceRecord{rdata: %Types.SOA{} = soa} -> soa
_ -> nil
end
end
defp filter_records(records, type) do
Enum.filter(records, fn rr -> rr_type_atom(rr) == type end)
end
defp pick_records(records, name, zone_name) do
direct = Enum.filter(records, fn rr -> rr.name == name end)
if direct != [] do
{:ok, direct}
else
wildcard = Enum.filter(records, fn rr -> rr.name == ["*" | zone_name] end)
if wildcard != [], do: {:ok, wildcard}, else: :error
end
end
defp lookup_in_zone(zone, name, zone_name) do
case Map.fetch(zone, zone_name) do
{:ok, records} ->
case pick_records(records, name, zone_name) do
{:ok, _} = ok -> {:halt, ok}
:error -> {:halt, :error}
end
:error ->
{:cont, :error}
end
end
defp rr_type_atom(%ResourceRecord{type: type}) when is_atom(type), do: type
defp rr_type_atom(%ResourceRecord{type: type}) when is_integer(type), do: RRType.from_code(type)
defp rr_type_atom(%ResourceRecord{rdata: %Types.A{}}), do: :a
defp rr_type_atom(%ResourceRecord{rdata: %Types.AAAA{}}), do: :aaaa
defp rr_type_atom(%ResourceRecord{rdata: %Types.CNAME{}}), do: :cname
defp rr_type_atom(%ResourceRecord{rdata: %Types.NS{}}), do: :ns
defp rr_type_atom(%ResourceRecord{rdata: %Types.TXT{}}), do: :txt
defp rr_type_atom(%ResourceRecord{rdata: %Types.MX{}}), do: :mx
defp rr_type_atom(%ResourceRecord{rdata: %Types.SRV{}}), do: :srv
defp rr_type_atom(%ResourceRecord{rdata: %Types.CAA{}}), do: :caa
defp rr_type_atom(%ResourceRecord{rdata: %Types.SOA{}}), do: :soa
defp rr_type_atom(_), do: nil
defp suffixes(labels) when is_list(labels) do
Enum.with_index(labels) |> Enum.map(fn {_item, index} -> Enum.drop(labels, index) end)
end
end