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