Commit 9b865d79 authored by Ulysse Beaugnon's avatar Ulysse Beaugnon

Merge branch 'master' of https://git.erp5.org/repos/vifibnet

parents 28bcfc52 d58795d5
...@@ -132,15 +132,18 @@ OPTIONS : VIFIBNET.PY ...@@ -132,15 +132,18 @@ OPTIONS : VIFIBNET.PY
-v, --verbose level -v, --verbose level
Defines the verbose level, level should be an integer between 0 Defines the verbose level, level should be an integer between 0
and 5 ( including ). There is no precise convention for verbode and 3 ( including ). There is no precise convention for verbode
level for now, except an increased number means more log messages. level for now, except an increased number means more log messages.
This parameter is also given to openvpn and babel for their log. This parameter is also given to babel for its log.
To adjust verbose level for openvpn, add an openvpn optional
arguments at the end of the command line
Default : 0 Default : 0
--registry address --registry address
Complete public ( reachable from the internet ) address of the machine Complete public ( reachable from the internet ) address of the machine
running a registry. Will be used to get the pirvate address of the running a registry. Will be used to get the pirvate address of the
registry and/or bootstrap peers registry and/or bootstrap peers
Examples : http://192.0.2.42:80, http://[2001:db8:42::1]:80
--hello duration --hello duration
Set hello interval, in seconds, for both wired and wireless Set hello interval, in seconds, for both wired and wireless
...@@ -253,4 +256,4 @@ New log system : ...@@ -253,4 +256,4 @@ New log system :
Note : logging.exception prints informations similar to pdb.set_trace() Note : logging.exception prints informations similar to pdb.set_trace()
info, which is pretty heavy, so for exception we expect ( for info, which is pretty heavy, so for exception we expect ( for
instance, connection problems to the registry ), one can print the instance, connection problems to the registry ), one can print the
exception as warning. ( see db.refresh() ). exception as warning. ( see db.refresh() ).
\ No newline at end of file
...@@ -10,4 +10,8 @@ To be done : ...@@ -10,4 +10,8 @@ To be done :
If we do this, we must protect some tunnels If we do this, we must protect some tunnels
To be discussed: To be discussed:
Project name ? Resinet/resnet/rsnet Project name ?
Resinet/res(6)net/rs(6)net
ResiliAnt ( ants : find shortest paths, don't die easily
~ are resilients )
rescan6
...@@ -4,7 +4,7 @@ import utils ...@@ -4,7 +4,7 @@ import utils
class PeerManager: class PeerManager:
# internal ip = temp arg/attribute # internal ip = temp arg/attribute
def __init__(self, db_dir_path, registry, key_path, refresh_time, address, def __init__(self, db_path, registry, key_path, refresh_time, address,
internal_ip, prefix, manual, pp , db_size): internal_ip, prefix, manual, pp , db_size):
self._refresh_time = refresh_time self._refresh_time = refresh_time
self._address = address self._address = address
...@@ -17,8 +17,7 @@ class PeerManager: ...@@ -17,8 +17,7 @@ class PeerManager:
self._manual = manual self._manual = manual
logging.info('Connecting to peers database...') logging.info('Connecting to peers database...')
self._db = sqlite3.connect(os.path.join(db_dir_path, 'peers.db'), self._db = sqlite3.connect(db_path, isolation_level=None)
isolation_level=None)
logging.debug('Database opened') logging.debug('Database opened')
logging.info('Preparing peers database...') logging.info('Preparing peers database...')
...@@ -123,8 +122,8 @@ class PeerManager: ...@@ -123,8 +122,8 @@ class PeerManager:
logging.debug('Boot peer received from server') logging.debug('Boot peer received from server')
p = subprocess.Popen(('openssl', 'rsautl', '-decrypt', '-inkey', self._key_path), p = subprocess.Popen(('openssl', 'rsautl', '-decrypt', '-inkey', self._key_path),
stdin=subprocess.PIPE, stdout=subprocess.PIPE) stdin=subprocess.PIPE, stdout=subprocess.PIPE)
bootpeer = p.communicate(bootpeer).split() bootpeer = p.communicate(bootpeer)[0].split()
self.db.execute("INSERT INTO peers (prefix, address) VALUES (?,?)", bootpeer) self._db.execute("INSERT INTO peers (prefix, address) VALUES (?,?)", bootpeer)
logging.debug('Boot peer added') logging.debug('Boot peer added')
return True return True
except socket.error: except socket.error:
...@@ -163,7 +162,7 @@ class PeerManager: ...@@ -163,7 +162,7 @@ class PeerManager:
if not self._manual: if not self._manual:
external_ip = arg external_ip = arg
new_address = list([external_ip, port, proto] new_address = list([external_ip, port, proto]
for port, proto in self._pp) for port, proto, _ in self._pp)
if self._address != new_address: if self._address != new_address:
self._address = new_address self._address = new_address
logging.info('Received new external ip : %s' logging.info('Received new external ip : %s'
......
...@@ -12,7 +12,6 @@ def openvpn(hello_interval, *args, **kw): ...@@ -12,7 +12,6 @@ def openvpn(hello_interval, *args, **kw):
'--user', 'nobody', '--user', 'nobody',
'--ping-exit', str(4 * hello_interval), '--ping-exit', str(4 * hello_interval),
'--group', 'nogroup', '--group', 'nogroup',
'--verb', str(verbose),
] + list(args) ] + list(args)
logging.trace('%s' % (args,)) logging.trace('%s' % (args,))
return subprocess.Popen(args, **kw) return subprocess.Popen(args, **kw)
......
#!/usr/bin/env python #!/usr/bin/env python
import argparse, math, random, select, smtplib, sqlite3, string, socket import argparse, math, random, select, smtplib, sqlite3, string, socket
import subprocess, time, threading, traceback, errno import subprocess, time, threading, traceback, errno, logging, os, xmlrpclib
from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
from email.mime.text import MIMEText from email.mime.text import MIMEText
from OpenSSL import crypto from OpenSSL import crypto
...@@ -41,6 +41,8 @@ class main(object): ...@@ -41,6 +41,8 @@ class main(object):
self.refresh_interval = 600 self.refresh_interval = 600
self.last_refresh = time.time() self.last_refresh = time.time()
utils.setupLog(3)
# Command line parsing # Command line parsing
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Peer discovery http server for vifibnet') description='Peer discovery http server for vifibnet')
...@@ -91,7 +93,7 @@ class main(object): ...@@ -91,7 +93,7 @@ class main(object):
self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read())
# Get vpn network prefix # Get vpn network prefix
self.network = bin(self.ca.get_serial_number())[3:] self.network = bin(self.ca.get_serial_number())[3:]
print "Network prefix : %s/%u" % (self.network, len(self.network)) logging.info("Network prefix : %s/%u" % (self.network, len(self.network)))
# Starting server # Starting server
server4 = SimpleXMLRPCServer4(('0.0.0.0', self.config.port), requestHandler=RequestHandler, allow_none=True) server4 = SimpleXMLRPCServer4(('0.0.0.0', self.config.port), requestHandler=RequestHandler, allow_none=True)
...@@ -124,7 +126,7 @@ class main(object): ...@@ -124,7 +126,7 @@ class main(object):
# Creating and sending email # Creating and sending email
s = smtplib.SMTP(self.config.mailhost) s = smtplib.SMTP(self.config.mailhost)
me = 'postmaster@vifibnet.com' me = 'postmaster@vifibnet.com'
msg = MIMEText('Hello world !\nYour token : %s' % (token,)) # XXX msg = MIMEText('Hello world !\nYour token : %s' % (token,)) # XXX
msg['Subject'] = '[Vifibnet] Token Request' msg['Subject'] = '[Vifibnet] Token Request'
msg['From'] = me msg['From'] = me
msg['To'] = email msg['To'] = email
...@@ -132,16 +134,22 @@ class main(object): ...@@ -132,16 +134,22 @@ class main(object):
s.quit() s.quit()
def _getPrefix(self, prefix_len): def _getPrefix(self, prefix_len):
assert 0 < prefix_len <= 128 - len(self.network) max_len = 128 - len(self.network)
for prefix, in self.db.execute("""SELECT prefix FROM vpn WHERE length(prefix) <= ? AND cert is null assert 0 < prefix_len <= max_len
ORDER BY length(prefix) DESC""", (prefix_len,)): try:
while len(prefix) < prefix_len: prefix, = self.db.execute("""SELECT prefix FROM vpn WHERE length(prefix) <= ? AND cert is null
self.db.execute("UPDATE vpn SET prefix = ? WHERE prefix = ?", (prefix + '1', prefix)) ORDER BY length(prefix) DESC""", (prefix_len,)).next()
prefix += '0' except StopIteration:
self.db.execute("INSERT INTO vpn VALUES (?,null,null)", (prefix,)) logging.error('There are no more free /%s prefix available' % (prefix_len,))
raise
while len(prefix) < prefix_len:
self.db.execute("UPDATE vpn SET prefix = ? WHERE prefix = ?", (prefix + '1', prefix))
prefix += '0'
self.db.execute("INSERT INTO vpn VALUES (?,null,null)", (prefix,))
if len(prefix) < max_len or '1' in prefix:
return prefix return prefix
logging.error('There are no more free /%s prefix available' % (prefix_len,)) self.db.execute("UPDATE vpn SET cert = 'reserved' WHERE prefix = ?", (prefix,))
raise RuntimeError return self._getPrefix(prefix_len)
def requestCertificate(self, handler, token, cert_req): def requestCertificate(self, handler, token, cert_req):
try: try:
...@@ -189,23 +197,28 @@ class main(object): ...@@ -189,23 +197,28 @@ class main(object):
FROM peers ORDER BY random() LIMIT 1""").next() FROM peers ORDER BY random() LIMIT 1""").next()
def getBootstrapPeer(self, handler, client_prefix): def getBootstrapPeer(self, handler, client_prefix):
cert = self.db.execute("SELECT cert FROM vpn WHERE prefix = ?", (client_prefix,)) cert, = self.db.execute("SELECT cert FROM vpn WHERE prefix = ?",
(client_prefix,)).next()
logging.trace('Getting bootpeer info...')
if self.config.bootstrap: if self.config.bootstrap:
bootpeer = random.choice(self.config.bootstrap) bootpeer = random.choice(self.config.bootstrap)
try: try:
prefix, address = self.db.execute("""SELECT prefix, address prefix, address = self.db.execute("""SELECT prefix, address
FROM peers WHERE prefix = ?""", (bootpeer,)) FROM peers WHERE prefix = ?""", (bootpeer,)).next()
except StopIteration: except StopIteration:
logging.info('Bootstrap peer %s unknown, sending random peer'
% hex(int(bootpeer, 2))[2:])
prefix, address = self._randomPeer() prefix, address = self._randomPeer()
else: else:
prefix, address = self._randomPeer() prefix, address = self._randomPeer()
logging.trace('Gotten bootpeer info from db')
r, w = os.pipe() r, w = os.pipe()
try: try:
threading.Thread(target=os.write, args=(w, cert)).start() threading.Thread(target=os.write, args=(w, cert)).start()
p = subprocess.Popen(('openssl', 'rsautl', '-encrypt', '-certin', '-inkey', '/proc/self/fd/%u' % r), p = subprocess.Popen(('openssl', 'rsautl', '-encrypt', '-certin', '-inkey', '/proc/self/fd/%u' % r),
stdin=subprocess.PIPE, stdout=subprocess.PIPE) stdin=subprocess.PIPE, stdout=subprocess.PIPE)
print "Sending bootstrap peer (%s, %s)" % (prefix, address) logging.info("Sending bootstrap peer (%s, %s)" % (prefix, address))
return xmlrpclib.Binary(p.communicate('%s %s' % (prefix, address))) return xmlrpclib.Binary(p.communicate('%s %s' % (prefix, address))[0])
finally: finally:
os.close(r) os.close(r)
os.close(w) os.close(w)
......
...@@ -8,10 +8,6 @@ def main(): ...@@ -8,10 +8,6 @@ def main():
_ = parser.add_argument _ = parser.add_argument
_('--ca-only', action='store_true', _('--ca-only', action='store_true',
help='To only get CA form server') help='To only get CA form server')
_('--db-only', action='store_true',
help='To only get CA and setup peer db with bootstrap peer')
_('--no-boot', action='store_true',
help='Enable to skip getting bootstrap peer')
_('--server', required=True, _('--server', required=True,
help='Address of the server delivering certifiactes') help='Address of the server delivering certifiactes')
_('--port', required=True, type=int, _('--port', required=True, type=int,
...@@ -20,6 +16,8 @@ def main(): ...@@ -20,6 +16,8 @@ def main():
help='Directory where the key and certificate will be stored') help='Directory where the key and certificate will be stored')
_('-r', '--req', nargs=2, action='append', _('-r', '--req', nargs=2, action='append',
help='Name and value of certificate request additional arguments') help='Name and value of certificate request additional arguments')
_('--email', help='Your email address')
_('--token', help='The token you received')
config = parser.parse_args() config = parser.parse_args()
# Establish connection with server # Establish connection with server
...@@ -33,33 +31,12 @@ def main(): ...@@ -33,33 +31,12 @@ def main():
if config.ca_only: if config.ca_only:
sys.exit(0) sys.exit(0)
# Create and initialize peers DB
db = sqlite3.connect(os.path.join(config.dir, 'peers.db'), isolation_level=None)
try:
db.execute("""CREATE TABLE peers (
prefix TEXT PRIMARY KEY,
address TEXT NOT NULL,
used INTEGER NOT NULL DEFAULT 0,
date INTEGER DEFAULT (strftime('%s', 'now')))""")
db.execute("CREATE INDEX _peers_used ON peers(used)")
except sqlite3.OperationalError, e:
if e.args[0] == 'table peers already exists':
print "Table peers already exists, leaving it as it is"
else:
print "sqlite3.OperationalError :" + e.args[0]
sys.exit(1)
if not config.no_boot:
prefix, address = s.getBootstrapPeer()
db.execute("INSERT INTO peers (prefix, address) VALUES (?,?)", (prefix, address))
if config.db_only:
sys.exit(0)
# Get token # Get token
email = raw_input('Please enter your email address : ') if not config.token:
_ = s.requestToken(email) if not config.email:
token = raw_input('Please enter your token : ') config.email = raw_input('Please enter your email address : ')
_ = s.requestToken(config.email)
config.token = raw_input('Please enter your token : ')
# Generate key and cert request # Generate key and cert request
pkey = crypto.PKey() pkey = crypto.PKey()
...@@ -76,7 +53,7 @@ def main(): ...@@ -76,7 +53,7 @@ def main():
req = crypto.dump_certificate_request(crypto.FILETYPE_PEM, req) req = crypto.dump_certificate_request(crypto.FILETYPE_PEM, req)
# Get certificate # Get certificate
cert = s.requestCertificate(token, req) cert = s.requestCertificate(config.token, req)
# Store cert and key # Store cert and key
with open(os.path.join(config.dir, 'cert.key'), 'w') as f: with open(os.path.join(config.dir, 'cert.key'), 'w') as f:
......
...@@ -25,7 +25,6 @@ class Connection: ...@@ -25,7 +25,6 @@ class Connection:
self.bandwidth = None self.bandwidth = None
self._last_trafic = None self._last_trafic = None
# TODO : update the stats
def refresh(self): def refresh(self):
# Check that the connection is alive # Check that the connection is alive
if self.process.poll() != None: if self.process.poll() != None:
...@@ -72,7 +71,7 @@ class Connection: ...@@ -72,7 +71,7 @@ class Connection:
class TunnelManager: class TunnelManager:
def __init__(self, write_pipe, peer_db, openvpn_args, hello_interval, def __init__(self, write_pipe, peer_db, openvpn_args, hello_interval,
refresh, connection_count, refresh_rate, iface_list, network): refresh, connection_count, refresh_ratio, iface_list, network):
self._write_pipe = write_pipe self._write_pipe = write_pipe
self._peer_db = peer_db self._peer_db = peer_db
self._connection_dict = {} self._connection_dict = {}
...@@ -83,14 +82,12 @@ class TunnelManager: ...@@ -83,14 +82,12 @@ class TunnelManager:
self._network = network self._network = network
self._net_len = len(network) self._net_len = len(network)
self._iface_list = iface_list self._iface_list = iface_list
self.free_interface_set = set(('client1', 'client2', 'client3',
'client4', 'client5', 'client6',
'client7', 'client8', 'client9',
'client10', 'client11', 'client12'))
self.next_refresh = time.time() self.next_refresh = time.time()
self._client_count = int(math.ceil(float(connection_count) / 2.0)) self._client_count = (connection_count + 1) // 2
self._refresh_count = int(math.ceil(refresh_rate * self._client_count)) self._refresh_count = int(math.ceil(refresh_ratio * self._client_count))
self.free_interface_set = set('client' + str(i)
for i in xrange(1, self._client_count + 1))
def refresh(self): def refresh(self):
logging.info('Refreshing the tunnels...') logging.info('Refreshing the tunnels...')
......
import argparse, time, struct, socket import argparse, time, struct, socket, logging
from OpenSSL import crypto from OpenSSL import crypto
verbose = 0 logging_levels = logging.WARNING, logging.INFO, logging.DEBUG, 5
def log(message, verbose_level): def setupLog(log_level):
if verbose >= verbose_level: logging.basicConfig(level=logging_levels[log_level],
print time.strftime("%d-%m-%Y %H:%M:%S :"), format='%(asctime)s : %(message)s',
print message datefmt='%d-%m-%Y %H:%M:%S')
logging.addLevelName(5, 'TRACE')
logging.trace = lambda *args, **kw: logging.log(5, *args, **kw)
def binFromIp(ip): def binFromIp(ip):
ip1, ip2 = struct.unpack('>QQ', socket.inet_pton(socket.AF_INET6, ip)) ip1, ip2 = struct.unpack('>QQ', socket.inet_pton(socket.AF_INET6, ip))
...@@ -21,7 +23,7 @@ def ipFromBin(prefix): ...@@ -21,7 +23,7 @@ def ipFromBin(prefix):
def ipFromPrefix(vifibnet, prefix, prefix_len): def ipFromPrefix(vifibnet, prefix, prefix_len):
prefix = bin(int(prefix))[2:].rjust(prefix_len, '0') prefix = bin(int(prefix))[2:].rjust(prefix_len, '0')
ip_t = (vifibnet + prefix).ljust(128, '0') ip_t = (vifibnet + prefix).ljust(127, '0').ljust(128, '1')
return ipFromBin(ip_t), prefix return ipFromBin(ip_t), prefix
def networkFromCa(ca_path): def networkFromCa(ca_path):
......
...@@ -89,18 +89,18 @@ def main(): ...@@ -89,18 +89,18 @@ def main():
config = getConfig() config = getConfig()
if not config.pp: if not config.pp:
config.pp = [['1194', 'udp'], ['1194', 'tcp-server']] config.pp = [['1194', 'udp'], ['1194', 'tcp-server']]
config.pp = list((port, proto, 'vifibnet-%s' % proto)
for port, proto in config.pp)
manual = bool(config.address) manual = bool(config.address)
network = utils.networkFromCa(config.ca) network = utils.networkFromCa(config.ca)
internal_ip, prefix = utils.ipFromCert(network, config.cert) internal_ip, prefix = utils.ipFromCert(network, config.cert)
openvpn_args = ovpnArgs(config.openvpn_args, config.ca, config.cert, openvpn_args = ovpnArgs(config.openvpn_args, config.ca, config.cert,
config.key) config.key)
db_path = os.path.join(config.state, 'peers.db')
# Set logging # Set logging
logging.basicConfig(level=logging.DEBUG, utils.setupLog(config.verbose)
format='%(asctime)s : %(message)s',
datefmt='%d-%m-%Y %H:%M:%S')
logging.addLevelName(5, 'TRACE')
logging.trace = lambda *args, **kw: logging.log(5, *args, **kw)
logging.trace("Configuration :\n%s" % config) logging.trace("Configuration :\n%s" % config)
# Set global variables # Set global variables
...@@ -120,20 +120,23 @@ def main(): ...@@ -120,20 +120,23 @@ def main():
for c, s in ('udp', 'udp'), ('tcp-client', 'tcp-server'): for c, s in ('udp', 'udp'), ('tcp-client', 'tcp-server'):
if len(list(x for x in config.address if x[2] == c)) \ if len(list(x for x in config.address if x[2] == c)) \
< len(list(x for x in config.pp if x[1] == s)): < len(list(x for x in config.pp if x[1] == s)):
pass # XXX: warn user about probable misconfiguration logging.warning("""Beware: in manual configuration, you
declared less external configurations regarding
protocol %s/%s than you gave internal server
configurations""" % (c, s))
else: else:
logging.info('Attempting automatic configuration via UPnP...') logging.info('Attempting automatic configuration via UPnP...')
try: try:
forwarder = upnpigd.Forwarder() forwarder = upnpigd.Forwarder()
config.address = [] config.address = []
for port, proto in config.pp: for port, proto, _ in config.pp:
ext = forwarder.AddRule(port, proto) ext = forwarder.AddRule(port, proto)
if ext: if ext:
config.address.append(ext) config.address.append(ext)
except upnpigd.NoUPnPDevice: except upnpigd.NoUPnPDevice:
logging.info('No upnp device found') logging.info('No upnp device found')
peer_db = db.PeerManager(config.state, config.registry, config.key, peer_db = db.PeerManager(db_path, config.registry, config.key,
config.peers_db_refresh, config.address, internal_ip, prefix, config.peers_db_refresh, config.address, internal_ip, prefix,
manual, config.pp, 200) manual, config.pp, 200)
tunnel_manager = tunnel.TunnelManager(write_pipe, peer_db, openvpn_args, tunnel_manager = tunnel.TunnelManager(write_pipe, peer_db, openvpn_args,
...@@ -141,22 +144,23 @@ def main(): ...@@ -141,22 +144,23 @@ def main():
config.refresh_ratio, config.iface_list, network) config.refresh_ratio, config.iface_list, network)
# Launch routing protocol. WARNING : you have to be root to start babeld # Launch routing protocol. WARNING : you have to be root to start babeld
interface_list = ['vifibnet'] + list(tunnel_manager.free_interface_set) \ interface_list = list(tunnel_manager.free_interface_set) \
+ config.iface_list + config.iface_list + list(iface
for _, _, iface in config.pp)
router = plib.router(network, internal_ip, interface_list, config.wireless, router = plib.router(network, internal_ip, interface_list, config.wireless,
config.hello, os.path.join(config.state, 'vifibnet.babeld.state'), config.hello, os.path.join(config.state, 'babeld.state'),
stdout=os.open(os.path.join(config.log, 'vifibnet.babeld.log'), stdout=os.open(os.path.join(config.log, 'babeld.log'),
os.O_WRONLY | os.O_CREAT | os.O_TRUNC), stderr=subprocess.STDOUT) os.O_WRONLY | os.O_CREAT | os.O_TRUNC), stderr=subprocess.STDOUT)
# Establish connections # Establish connections
server_process = list(plib.server(internal_ip, len(network) + len(prefix), server_process = list(plib.server(internal_ip, len(network) + len(prefix),
config.connection_count, config.dh, write_pipe, port, config.connection_count, config.dh, write_pipe, port,
proto, config.hello, '--dev', 'vifibnet-%s' % proto, *openvpn_args, proto, config.hello, '--dev', iface, *openvpn_args,
stdout=os.open(os.path.join(config.log, stdout=os.open(os.path.join(config.log,
'vifibnet.server.%s.log' % (proto,)), 'vifibnet.server.%s.log' % (proto,)),
os.O_WRONLY | os.O_CREAT | os.O_TRUNC), os.O_WRONLY | os.O_CREAT | os.O_TRUNC),
stderr=subprocess.STDOUT) stderr=subprocess.STDOUT)
for port, proto in config.pp) for port, proto, iface in config.pp)
tunnel_manager.refresh() tunnel_manager.refresh()
# main loop # main loop
...@@ -190,7 +194,6 @@ def main(): ...@@ -190,7 +194,6 @@ def main():
pass pass
except sqlite3.Error: except sqlite3.Error:
traceback.print_exc() traceback.print_exc()
db_path = os.path.join(config.state, 'peers.db')
os.rename(db_path, db_path + '.bak') os.rename(db_path, db_path + '.bak')
os.execvp(sys.executable, sys.argv) os.execvp(sys.executable, sys.argv)
except KeyboardInterrupt: except KeyboardInterrupt:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment