Add tests and support for blacklisting IPs
[nip.io] / nipio / backend.py
1 #!/usr/bin/python
2
3 import ConfigParser
4 import os
5 import re
6 import sys
7
8
9 def _is_debug():
10     return False
11
12
13 def _log(msg):
14     sys.stderr.write('backend (%s): %s\n' % (os.getpid(), msg))
15
16
17 def _write(*l):
18     args = len(l)
19     c = 0
20     for a in l:
21         c += 1
22         if _is_debug():
23             _log('writing: %s' % a)
24         sys.stdout.write(a)
25         if c < args:
26             if _is_debug():
27                 _log('writetab')
28             sys.stdout.write('\t')
29     if _is_debug():
30         _log('writenewline')
31     sys.stdout.write('\n')
32     sys.stdout.flush()
33
34
35 def _get_next():
36     if _is_debug():
37         _log('reading now')
38     line = sys.stdin.readline()
39     if _is_debug():
40         _log('read line: %s' % line)
41     return line.strip().split('\t')
42
43
44 class DynamicBackend:
45     def __init__(self):
46         self.id = ''
47         self.soa = ''
48         self.domain = ''
49         self.ip_address = ''
50         self.ttl = ''
51         self.name_servers = {}
52         self.blacklisted_ips = []
53
54     def configure(self):
55         fname = self._get_config_filename()
56         if not os.path.exists(fname):
57             _log('%s does not exist' % fname)
58             sys.exit(1)
59
60         with open(fname) as fp:
61             config = ConfigParser.ConfigParser()
62             config.readfp(fp)
63
64         self.id = config.get('soa', 'id')
65         self.soa = '%s %s %s' % (config.get('soa', 'ns'), config.get('soa', 'hostmaster'), self.id)
66         self.domain = config.get('main', 'domain')
67         self.ip_address = config.get('main', 'ipaddress')
68         self.ttl = config.get('main', 'ttl')
69
70         for entry in config.items('nameservers'):
71             self.name_servers[entry[0]] = entry[1]
72
73         if config.has_section("blacklist"):
74             for entry in config.items("blacklist"):
75                 self.blacklisted_ips.append(entry[1])
76
77         _log('Name servers: %s' % self.name_servers)
78         _log('ID: %s' % self.id)
79         _log('TTL %s' % self.ttl)
80         _log('SOA: %s' % self.soa)
81         _log('IP Address: %s' % self.ip_address)
82         _log('DOMAIN: %s' % self.domain)
83         _log("Blacklist: %s" % self.blacklisted_ips)
84
85     def run(self):
86         _log('starting up')
87         handshake = _get_next()
88         if handshake[1] != '1':
89             _log('Not version 1: %s' % handshake)
90             sys.exit(1)
91         _write('OK', 'We are good')
92         _log('Done handshake')
93
94         while True:
95             cmd = _get_next()
96             if _is_debug():
97                 _log("cmd: %s" % cmd)
98
99             if cmd[0] == "END":
100                 _log("completing")
101                 break
102
103             if len(cmd) < 6:
104                 _log('did not understand: %s' % cmd)
105                 _write('FAIL')
106                 continue
107
108             qname = cmd[1].lower()
109             qtype = cmd[3]
110
111             if (qtype == 'A' or qtype == 'ANY') and qname.endswith(self.domain):
112                 if qname == self.domain:
113                     self.handle_self(self.domain)
114                 elif qname in self.name_servers:
115                     self.handle_nameservers(qname)
116                 else:
117                     self.handle_subdomains(qname)
118             elif qtype == 'SOA' and qname.endswith(self.domain):
119                 self.handle_soa(qname)
120             else:
121                 self.handle_unknown(qtype, qname)
122
123     def handle_self(self, name):
124         _write('DATA', name, 'IN', 'A', self.ttl, self.id, self.ip_address)
125         self.write_name_servers(name)
126         _write('END')
127
128     def handle_subdomains(self, qname):
129         subdomain = qname[0:qname.find(self.domain) - 1]
130
131         subparts = subdomain.split('.')
132         if len(subparts) < 4:
133             if _is_debug():
134                 _log('subparts less than 4')
135             self.handle_self(qname)
136             return
137
138         ip_address_parts = subparts[-4:]
139         if _is_debug():
140             _log('ip: %s' % ip_address_parts)
141         for part in ip_address_parts:
142             if re.match('^\d{1,3}$', part) is None:
143                 if _is_debug():
144                     _log('%s is not a number' % part)
145                 self.handle_self(qname)
146                 return
147             parti = int(part)
148             if parti < 0 or parti > 255:
149                 if _is_debug():
150                     _log('%d is too big/small' % parti)
151                 self.handle_self(qname)
152                 return
153
154         ip_address = ".".join(ip_address_parts)
155         if ip_address in self.blacklisted_ips:
156             self.handle_blacklisted(ip_address)
157             return
158
159         _write('DATA', qname, 'IN', 'A', self.ttl, self.id, '%s.%s.%s.%s' % (ip_address_parts[0], ip_address_parts[1], ip_address_parts[2], ip_address_parts[3]))
160         self.write_name_servers(qname)
161         _write('END')
162
163     def handle_nameservers(self, qname):
164         ip = self.name_servers[qname]
165         _write('DATA', qname, 'IN', 'A', self.ttl, self.id, ip)
166         _write('END')
167
168     def write_name_servers(self, qname):
169         for nameServer in self.name_servers:
170             _write('DATA', qname, 'IN', 'NS', self.ttl, self.id, nameServer)
171
172     def handle_soa(self, qname):
173         _write('DATA', qname, 'IN', 'SOA', self.ttl, self.id, self.soa)
174         _write('END')
175
176     def handle_unknown(self, qtype, qname):
177         _write('LOG', 'Unknown type: %s, domain: %s' % (qtype, qname))
178         _write('END')
179
180     def handle_blacklisted(self, ip_address):
181         _write('LOG', 'Blacklisted: %s' % ip_address)
182         _write('END')
183
184     def _get_config_filename(self):
185         return os.path.join(os.path.dirname(os.path.realpath(__file__)), 'backend.conf')
186
187
188 if __name__ == '__main__':
189     backend = DynamicBackend()
190     backend.configure()
191     backend.run()