Merge branch 'master' of https://git.erp5.org/repos/vifibnet
[re6stnet.git] / registry.py
1 #!/usr/bin/env python
2 import argparse, math, random, select, smtplib, sqlite3, string, socket, time, traceback, errno
3 from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
4 from email.mime.text import MIMEText
5 from OpenSSL import crypto
6 import utils
7
8
9
10 # To generate server ca and key with serial for 2001:db8:42::/48
11 # openssl req -nodes -new -x509 -key ca.key -set_serial 0x120010db80042 -days 365 -out ca.crt
12
13 IPV6_V6ONLY = 26
14 SOL_IPV6 = 41
15
16
17 class RequestHandler(SimpleXMLRPCRequestHandler):
18
19 def _dispatch(self, method, params):
20 return self.server._dispatch(method, (self,) + params)
21
22
23 class SimpleXMLRPCServer4(SimpleXMLRPCServer):
24
25 allow_reuse_address = True
26
27
28 class SimpleXMLRPCServer6(SimpleXMLRPCServer4):
29
30 address_family = socket.AF_INET6
31
32 def server_bind(self):
33 self.socket.setsockopt(SOL_IPV6, IPV6_V6ONLY, 1)
34 SimpleXMLRPCServer4.server_bind(self)
35
36
37 class main(object):
38
39 def __init__(self):
40 self.cert_duration = 365 * 86400
41 self.time_out = 86400
42 self.refresh_interval = 600
43 self.last_refresh = time.time()
44
45 # Command line parsing
46 parser = argparse.ArgumentParser(
47 description='Peer discovery http server for vifibnet')
48 _ = parser.add_argument
49 _('port', type=int, help='Port of the host server')
50 _('--db', required=True,
51 help='Path to database file')
52 _('--ca', required=True,
53 help='Path to ca.crt file')
54 _('--key', required=True,
55 help='Path to certificate key')
56 _('--mailhost', required=True,
57 help='SMTP server mail host')
58 _('--bootstrap', nargs=4, action="append",
59 help='''VPN prefix, ip address, port and protocol to send as
60 bootstrap peers, instead of random ones''')
61 self.config = parser.parse_args()
62
63 # Database initializing
64 self.db = sqlite3.connect(self.config.db, isolation_level=None)
65 self.db.execute("""CREATE TABLE IF NOT EXISTS peers (
66 prefix text primary key not null,
67 address text not null,
68 date integer default (strftime('%s','now')))""")
69 self.db.execute("CREATE INDEX IF NOT EXISTS peers_ping ON peers(date)")
70 self.db.execute("""CREATE TABLE IF NOT EXISTS tokens (
71 token text primary key not null,
72 email text not null,
73 prefix_len integer not null,
74 date integer not null)""")
75 try:
76 self.db.execute("""CREATE TABLE vpn (
77 prefix text primary key not null,
78 email text,
79 cert text)""")
80 except sqlite3.OperationalError, e:
81 if e.args[0] != 'table vpn already exists':
82 raise RuntimeError
83 else:
84 self.db.execute("INSERT INTO vpn VALUES ('',null,null)")
85
86 # Loading certificates
87 with open(self.config.ca) as f:
88 self.ca = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
89 with open(self.config.key) as f:
90 self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read())
91 # Get vpn network prefix
92 self.network = bin(self.ca.get_serial_number())[3:]
93 print "Network prefix : %s/%u" % (self.network, len(self.network))
94
95 # Starting server
96 server4 = SimpleXMLRPCServer4(('0.0.0.0', self.config.port), requestHandler=RequestHandler, allow_none=True)
97 server4.register_instance(self)
98 server6 = SimpleXMLRPCServer6(('::', self.config.port), requestHandler=RequestHandler, allow_none=True)
99 server6.register_instance(self)
100
101 # Main loop
102 while True:
103 try:
104 r, w, e = select.select([server4, server6], [], [])
105 except (OSError, select.error) as e:
106 if e.args[0] != errno.EINTR:
107 raise
108 else:
109 for r in r:
110 r._handle_request_noblock()
111
112 def requestToken(self, handler, email):
113 while True:
114 # Generating token
115 token = ''.join(random.sample(string.ascii_lowercase, 8))
116 # Updating database
117 try:
118 self.db.execute("INSERT INTO tokens VALUES (?,?,?,?)", (token, email, 16, int(time.time())))
119 break
120 except sqlite3.IntegrityError:
121 pass
122
123 # Creating and sending email
124 s = smtplib.SMTP(self.config.mailhost)
125 me = 'postmaster@vifibnet.com'
126 msg = MIMEText('Hello world !\nYour token : %s' % (token,))
127 msg['Subject'] = '[Vifibnet] Token Request'
128 msg['From'] = me
129 msg['To'] = email
130 s.sendmail(me, email, msg.as_string())
131 s.quit()
132
133 def _getPrefix(self, prefix_len):
134 assert 0 < prefix_len <= 128 - len(self.network)
135 for prefix, in self.db.execute("""SELECT prefix FROM vpn WHERE length(prefix) <= ? AND cert is null
136 ORDER BY length(prefix) DESC""", (prefix_len,)):
137 while len(prefix) < prefix_len:
138 self.db.execute("UPDATE vpn SET prefix = ? WHERE prefix = ?", (prefix + '1', prefix))
139 prefix += '0'
140 self.db.execute("INSERT INTO vpn VALUES (?,null,null)", (prefix,))
141 return prefix
142 raise RuntimeError # TODO: raise better exception
143
144 def requestCertificate(self, handler, token, cert_req):
145 try:
146 req = crypto.load_certificate_request(crypto.FILETYPE_PEM, cert_req)
147 with self.db:
148 try:
149 token, email, prefix_len, _ = self.db.execute("SELECT * FROM tokens WHERE token = ?", (token,)).next()
150 except StopIteration:
151 # TODO: return nice error message
152 raise
153 self.db.execute("DELETE FROM tokens WHERE token = ?", (token,))
154
155 # Get a new prefix
156 prefix = self._getPrefix(prefix_len)
157
158 # Create certificate
159 cert = crypto.X509()
160 #cert.set_serial_number(serial)
161 cert.gmtime_adj_notBefore(0)
162 cert.gmtime_adj_notAfter(self.cert_duration)
163 cert.set_issuer(self.ca.get_subject())
164 subject = req.get_subject()
165 subject.CN = "%u/%u" % (int(prefix, 2), prefix_len)
166 cert.set_subject(subject)
167 cert.set_pubkey(req.get_pubkey())
168 cert.sign(self.key, 'sha1')
169 cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
170
171 # Insert certificate into db
172 self.db.execute("UPDATE vpn SET email = ?, cert = ? WHERE prefix = ?", (email, cert, prefix))
173
174 return cert
175 except:
176 traceback.print_exc()
177 raise
178
179 def getCa(self, handler):
180 return crypto.dump_certificate(crypto.FILETYPE_PEM, self.ca)
181
182 def getBootstrapPeer(self, handler):
183 # TODO: Insert a flag column for bootstrap ready servers in peers
184 # ( servers which shouldn't go down or change ip and port as opposed to servers owned by particulars )
185 # that way, we also ascertain that the server sent is not the new node....
186 if self.config.bootstrap:
187 bootpeer = random.choice(self.config.bootstrap)
188 prefix = bootpeer[0]
189 address = ','.join(bootpeer[1:])
190 else:
191 prefix, address = self.db.execute("""SELECT prefix, address
192 FROM peers ORDER BY random() LIMIT 1""")
193 print "Sending bootstrap peer (%s, %s)" % (prefix, address)
194 return prefix, address
195
196 def declare(self, handler, address):
197 print "declaring new node"
198 client_address, address = address
199 #client_address, _ = handler.client_address
200 client_ip = utils.binFromIp(client_address)
201 if client_ip.startswith(self.network):
202 prefix = client_ip[len(self.network):]
203 prefix, = self.db.execute("SELECT prefix FROM vpn WHERE prefix <= ? ORDER BY prefix DESC LIMIT 1", (prefix,)).next()
204 self.db.execute("INSERT OR REPLACE INTO peers (prefix, address) VALUES (?,?)", (prefix, address))
205 return True
206 else:
207 # TODO: use log + DO NOT PRINT BINARY IP
208 print "Unauthorized connection from %s which does not start with %s" % (client_ip, self.network)
209 return False
210
211 def getPeerList(self, handler, n, client_address):
212 assert 0 < n < 1000
213 client_ip = utils.binFromIp(client_address)
214 if client_ip.startswith(self.network):
215 if time.time() > self.last_refresh + self.refresh_interval:
216 print "refreshing peers for dead ones"
217 self.db.execute("DELETE FROM peers WHERE ( date + ? ) <= CAST (strftime('%s', 'now') AS INTEGER)", (self.time_out,))
218 self.last_refesh = time.time()
219 print "sending peers"
220 return self.db.execute("SELECT prefix, address FROM peers ORDER BY random() LIMIT ?", (n,)).fetchall()
221 else:
222 # TODO: use log + DO NOT PRINT BINARY IP
223 print "Unauthorized connection from %s which does not start with %s" % (client_ip, self.network)
224 raise RuntimeError
225
226 if __name__ == "__main__":
227 main()