c30cab165b996399a157ba1aa045ac3a61b7a75a
[zonemaker.git] / zonemaker / zone.py
1 import re
2 from ipaddress import IPv4Address, IPv6Address
3 from typing import List, Dict, Any, Iterator
4
5
6 second = 1
7 minute = 60*second
8 hour = 60*minute
9 day = 24*hour
10 week = 7*day
11
12 def check_hostname(name: str) -> str:
13     # check hostname for validity
14     label = r'[a-zA-Z90-9]([a-zA-Z90-9-]{0,61}[a-zA-Z90-9])?' # must not start or end with hyphen
15     pattern = r'^{0}(\.{0})*\.?'.format(label)
16     if re.match(pattern, name):
17         return name
18     raise Exception(name+" is not a valid hostname")
19
20 def abs_hostname(name: str, root: str = None) -> str:
21     if name.endswith('.'):
22         return name
23     if root is None:
24         raise Exception("Name {0} was expected to be absolute, but it is not".format(name))
25     if not root.endswith('.'):
26         raise Exception("Root {0} was expected to be absolute, but it is not".format(name))
27     return name+"."+root
28
29 def time(time: int) -> str:
30     if time == 0:
31         return "0"
32     elif time % week == 0:
33         return str(time//week)+"w"
34     elif time % day == 0:
35         return str(time//day)+"d"
36     elif time % hour == 0:
37         return str(time//hour)+"h"
38     elif time % minute == 0:
39         return str(time//minute)+"m"
40     else:
41         return str(time)
42
43 class Address:
44     # mypy does not know about the ipaddress types, so leave this class unannotated for now
45     def __init__(self, IPv4 = None, IPv6 = None) -> None:
46         self._IPv4 = None if IPv4 is None else IPv4Address(IPv4)
47         self._IPv6 = None if IPv6 is None else IPv6Address(IPv6)
48     
49     def IPv4(self):
50         return Address(IPv4 = self._IPv4)
51     
52     def IPv6(self):
53         return Address(IPv6 = self._IPv6)
54
55 class Name:
56     def __init__(self, address: Address = None, MX: List = None,
57                  TCP: Dict[int, Any] = None, UDP: Dict[int, Any] = None) -> None:
58         self._address = address
59
60 class Service:
61     def __init__(self, SRV: str = None, TLSA: str=None) -> None:
62         self._SRV = None if SRV is None else check_hostname(SRV)
63         self._TLSA = TLSA
64
65 class CName:
66     def __init__(self, name: str) -> None:
67         self._name = check_hostname(name)
68
69 class Delegation():
70     def __init__(self, NS: str, DS: str = None) -> None:
71         pass
72
73 class RR():
74     def __init__(self, owner: str, TTL: int, recordType: str, data: str) -> None:
75         assert owner.endswith('.') # users have to make this absolute first
76         self._owner = owner
77         self._TTL = TTL
78         self._recordType = recordType
79         self._data = data
80     
81     def __str__(self) -> str:
82         return "{0}\t{1}\t{2}\t{3}".format(self._owner, self._TTL, self._recordType, self._data)
83
84 class Zone:
85     def __init__(self, name: str, mail: str, NS: List[str],
86                  secondary_refresh: int, secondary_retry: int, secondary_expire: int,
87                  NX_TTL: int = None, A_TTL: int = None, other_TTL: int = None,
88                  domains: Dict[str, Any] = {}) -> None:
89         self._name = check_hostname(name)
90         if not mail.endswith('.'): raise Exception("Mail must be absolute, end with a dot")
91         atpos = mail.find('@')
92         if atpos < 0 or atpos > mail.find('.'): raise Exception("Mail must contain an @ before the first dot")
93         self._mail = check_hostname(mail.replace('@', '.', 1))
94         self._NS = list(map(check_hostname, NS))
95         
96         self._refresh = secondary_refresh
97         self._retry = secondary_retry
98         self._expire = secondary_expire
99         
100         assert other_TTL is not None
101         self._NX_TTL = other_TTL if NX_TTL is None else NX_TTL
102         self._A_TTL = other_TTL if A_TTL is None else A_TTL
103         self._other_TTL = other_TTL
104     
105     def generate_rrs(self) -> Iterator[RR]:
106         serial = -1
107         yield (RR(abs_hostname(self._name), self._other_TTL, 'SOA',
108                   '{NS} {mail} ({serial} {refresh} {retry} {expire} {NX_TTL})'.format(
109                       NS=self._NS[0], mail=self._mail, serial=serial,
110                       refresh=time(self._refresh), retry=time(self._retry), expire=time(self._expire),
111                       NX_TTL=time(self._NX_TTL))
112                   ))
113     
114     def write(self, file:Any) -> None:
115         for rr in self.generate_rrs():
116             print(rr)