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