From 214cb0796212b9c8452151001ea9fa8e800e9c3b Mon Sep 17 00:00:00 2001 From: Maurizio Porrato Date: Sun, 30 Jan 2011 02:29:31 +0100 Subject: [PATCH] Add DB-based system manager server. New simple XML parser. --- frn/manager/__init__.py | 8 +-- frn/manager/database.py | 141 ++++++++++++++++++++++++++++++++++---- frn/manager/dummy.py | 10 +-- frn/manager/remote.py | 12 ++-- frn/protocol/manager.py | 31 +++++++-- frn/protocol/server.py | 30 ++++---- frn/userstore/__init__.py | 9 --- frn/userstore/database.py | 84 ----------------------- frn/utility.py | 41 +---------- manager.py | 32 +++++---- 10 files changed, 207 insertions(+), 191 deletions(-) delete mode 100644 frn/userstore/__init__.py delete mode 100644 frn/userstore/database.py diff --git a/frn/manager/__init__.py b/frn/manager/__init__.py index 006e692..0535cab 100644 --- a/frn/manager/__init__.py +++ b/frn/manager/__init__.py @@ -8,16 +8,16 @@ from zope.interface import Interface class IManager(Interface): - def serverLogin(user): + def serverLogin(server): """Logs server on""" - def serverLogout(user): + def serverLogout(server): """Logs server out""" - def clientLogin(user): + def clientLogin(server, user): """Logs client in""" - def clientLogout(user): + def clientLogout(server, user): """Logs client out""" def getClientList(): diff --git a/frn/manager/database.py b/frn/manager/database.py index c255b70..7183f64 100644 --- a/frn/manager/database.py +++ b/frn/manager/database.py @@ -5,33 +5,146 @@ from zope.interface import implements from frn.manager import IManager -from frn.userstore.database import DatabaseUserStore +from twisted.python import log +from twisted.python.failure import Failure +from twisted.mail.smtp import sendmail +import random, string, uuid +from frn.user import FRNUser +def rndpasswd(): + return ''.join([random.choice(string.ascii_uppercase) for i in range(8)]) class DatabaseManager(object): implements(IManager) - def __init__(self, store): - self._store = store + def __init__(self, pool): + self._pool = pool + self._pool.runOperation(""" + CREATE TABLE IF NOT EXISTS frn_users ( + _id VARCHAR(32) NOT NULL PRIMARY KEY, + _ea VARCHAR(30) UNIQUE NOT NULL, + _pw VARCHAR(20) NOT NULL, + _on VARCHAR(20) NOT NULL, + _bc VARCHAR(20) NOT NULL, + _nn VARCHAR(20) NOT NULL, + _ct VARCHAR(20) NOT NULL, + _nt VARCHAR(20), + _ds VARCHAR(20) NOT NULL, + _ip VARCHAR(20), + registration TIMESTAMP NOT NULL DEFAULT current_timestamp, + lastlogin TIMESTAMP, + server VARCHAR(20), + port INTEGER + ); + """) + self._pool.runOperation(""" + CREATE TABLE IF NOT EXISTS frn_servers ( + _vx VARCHAR(7) NOT NULL, + _sn VARCHAR(20) NOT NULL, + _pt INTEGER NOT NULL, + _bn VARCHAR(20), + _bp integer, + _ow VARCHAR(30) NOT NULL, + registration TIMESTAMP NOT NULL DEFAULT current_timestamp, + PRIMARY KEY(_sn, _pt) + ); + """) - def serverLogin(self, user): - pass + def serverLogin(self, server): + def checkauth(res): + if not isinstance(res, Failure): + if res[0] > 0: + return self._pool.runOperation(""" + INSERT INTO frn_servers + (_vx, _sn, _pt, _bn, _bp, _ow) VALUES + (?,?,?,?,?,?) + """, (server.VX, server.SN, server.PT, + server.BN, server.BP, server.OW)).addCallbacks( + lambda x: 0, lambda x: -1) + else: + return -1 + return self._pool.runQuery("SELECT count(*) FROM frn_users WHERE _ea=? AND _pw=?", + (server.OW, server.PW)).addBoth(checkauth) - def serverLogout(self, user): - pass + def serverLogout(self, server): + self._pool.runOperation("UPDATE frn_users SET server=NULL, port=NULL, _nt=NULL WHERE server=? AND port=?", + (server.SN, server.PT)) + self._pool.runOperation("DELETE FROM frn_servers WHERE _sn=? AND _pt=?", + (server.SN, server.PT)) - def clientLogin(self, user): - pass + def clientLogin(self, server, user): + def userfound(data): + log.msg("Client login: %s" % repr(data)) + if data: + u = dict(zip( + ('ID', 'EA', 'PW', 'ON', 'BC', 'NN', 'CT', 'NT', 'DS', 'IP', 'registration', 'lastlogin', 'server', 'port'), + data[0])) + if u['server']: + log.msg("Duplicate client %s" % repr(user)) + return "DUPL" + self._pool.runQuery(""" + UPDATE frn_users SET + _bc=?, _nn=?, _ct=?, _nt=?, _ds=?, _ip=?, server=?, port=?, + lastlogin=current_timestamp + WHERE _id=? + """, (user.BC, user.NN, user.CT, user.NT, user.DS, user.IP, server.SN, server.PT, u['ID'])) + log.msg("Authenticated client %s" % repr(user)) + return str(u['ID']) + else: + log.msg("Wrong client %s" % repr(user)) + return "WRONG" + return self._pool.runQuery("SELECT * FROM frn_users WHERE _ea=? AND _on=? AND _pw=?", + (user.EA, user.ON, user.PW)).addCallbacks(userfound, lambda x: "WRONG") - def clientLogout(self, user): - pass + def clientLogout(self, server, user): + log.msg("Logging out client %s" % repr(user)) + return self._pool.runOperation("UPDATE frn_users SET server=NULL, port=NULL, _nt=NULL WHERE _id=?", + (user.ID,)).addBoth(lambda x: "OK") def getClientList(self): - pass + def buildlist(tr): + tr.execute("SELECT _sn, _pt FROM frn_servers") + servers = tr.fetchall() + r = {} + for sn, sp in servers: + r[(sn,sp)] = {} + tr.execute("SELECT DISTINCT _nt FROM frn_users WHERE server=? AND port=?", (sn,sp)) + networks = tr.fetchall() + for (n,) in networks: + r[(sn,sp)][n] = [] + tr.execute("SELECT _id, _on, _bc, _ds, _nn, _ct FROM frn_users WHERE server=? AND port=? AND _nt=?", + (sn, sp, n)) + clients = tr.fetchall() + for c in clients: + cu = FRNUser(**dict(zip(['EA','ON','BC','DS','NN','CT'],c))) + r[(sn,sp)][n].append(cu) + return r + return self._pool.runInteraction(buildlist) def registerUser(self, user): - pass - + def fetchdata(is_new): + def maildata(data): + u = dict(zip( + ('ID', 'EA', 'PW', 'ON', 'BC', 'NN', 'CT', 'NT', 'DS', 'IP', 'registration', 'lastlogin', 'server', 'port'), + data[0])) + log.msg("Mailing password to user %s" % str(u)) + with open('mailtemplate.txt','r') as tplfile: + tpl = tplfile.read() + mailbody = string.Template(tpl).safe_substitute(u) + sendmail('127.0.0.1', + 'admin@gnuradionetwork.org', + [u['EA']], + mailbody, port=2525) + return "OK" + return self._pool.runQuery( + "SELECT * FROM frn_users WHERE _ea=?", (user.EA,) + ).addCallback(maildata).addErrback(lambda x: "ERROR") + return self._pool.runOperation(""" + INSERT INTO frn_users (_id, _ea, _pw, _on, _bc, _ds, _nn, _ct) + VALUES (?,?,?,?,?,?,?,?) + """, (uuid.uuid4().get_hex()[:20], user.EA, rndpasswd(), + user.ON, user.BC, user.DS, user.NN, user.CT) + ).addBoth(fetchdata) # vim: set et ai sw=4 ts=4 sts=4: diff --git a/frn/manager/dummy.py b/frn/manager/dummy.py index 1c3cacb..12223e8 100644 --- a/frn/manager/dummy.py +++ b/frn/manager/dummy.py @@ -16,20 +16,20 @@ class DummyManager(object): def _randId(self): return '.'.join([str(randint(1,254)) for i in range(4)]) - def serverLogin(self, user): + def serverLogin(self, server): return defer.succeed(0) - def serverLogout(self, user): + def serverLogout(self, server): return defer.succeed(None) - def clientLogin(self, user): + def clientLogin(self, server, user): return defer.succeed(self._randId()) - def clientLogout(self, user): + def clientLogout(self, server, user): return defer.succeed('OK') def getClientList(self): - return defer.succeed([]) + return defer.succeed({}) def registerUser(self, user): return defer.succeed('OK') diff --git a/frn/manager/remote.py b/frn/manager/remote.py index f8978fc..7e90063 100644 --- a/frn/manager/remote.py +++ b/frn/manager/remote.py @@ -55,23 +55,23 @@ class RemoteManager(object): def doConnect(self): self.reactor.connectTCP(self.server, self.port, self.factory) - def serverLogin(self, user): - self.factory = CustomManagerClientFactory(user) + def serverLogin(self, server): + self.factory = CustomManagerClientFactory(server) self.doConnect() return self.factory.deferred - def serverLogout(self, user): + def serverLogout(self, server): if self.factory.client is not None: - return self.factory.client.sendServerLogout(user) + return self.factory.client.sendServerLogout(server) - def clientLogin(self, user): + def clientLogin(self, server, user): if self.maskParrot and user.BC == 'Parrot': u = user.copy(BC='PC Only') else: u = user.copy() return self.factory.client.sendClientLogin(u) - def clientLogout(self, user): + def clientLogout(self, server, user): if self.maskParrot and user.BC == 'Parrot': u = user.copy(BC='PC Only') else: diff --git a/frn/protocol/manager.py b/frn/protocol/manager.py index 5986b82..b711473 100644 --- a/frn/protocol/manager.py +++ b/frn/protocol/manager.py @@ -112,12 +112,16 @@ class FRNManagerServer(LineOnlyReceiver): def connectionMade(self): log.msg("Manager client connected from %s" % self.transport.getPeer().host) self._phase = "CONNECTED" + self.serverInfo = None self.kp = makeRandomChallange() def connectionLost(self, reason): log.msg("Manager client disconnected from %s: %s" % (self.transport.getPeer().host, reason)) - self.manager.serverLogout(None) # FIXME + if self.serverInfo: + self.manager.serverLogout(self.serverInfo) + self.serverInfo = None + self._phase = "DISCONNECTED" def _authOnly(self): if self._phase != "AUTHENTICATED": @@ -129,6 +133,8 @@ class FRNManagerServer(LineOnlyReceiver): if self._phase == "CHALLANGE": if sline == responseToChallange(self.kp): self._phase = "AUTHENTICATED" + self.serverInfo = self.tmpServerInfo + log.msg("Auth success: %s" % repr(self.serverInfo)) else: self.transport.loseConnection() else: @@ -146,8 +152,19 @@ class FRNManagerServer(LineOnlyReceiver): handler(xbody) def sendClientList(self): + def gotClientList(cl): + self.sendLine(str(len(cl))) + for sn, sp in cl: + self.sendLine("%s - Port: %d" % (sn, sp)) + self.sendLine(str(len(cl[(sn,sp)]))) + for nt in cl[(sn,sp)]: + self.sendLine(nt) + self.sendLine(str(len(cl[(sn,sp)][nt]))) + for u in cl[(sn,sp)][nt]: + self.sendLine(u.asXML('EA','ON','BC','DS','NN','CT')) log.msg("SM") - self.sendLine('0') # TODO + return self.manager.getClientList().addCallback(gotClientList) + def unimplemented(self, cmd, body): log.err("Unimplemented command %s: %s" % (cmd, str(body))) @@ -156,13 +173,17 @@ class FRNManagerServer(LineOnlyReceiver): def sendManagerInfo(res): if versions.manager > 2009004: self._phase = "CHALLANGE" + self.tmpServerInfo = FRNUser(**body) else: self._phase = "AUTHENTICATED" + self.serverInfo = FRNUser(**body) + log.msg("Auth success: %s" % repr(self.serverInfo)) self.sendLine(formatSimpleXML([ ('SV', versions.server), ('CV', versions.client), ('MC', versions.manager), - ('AL', res['al']), +# ('AL', res['al']), + ('AL', res), ('KP', self.kp) ])) log.msg("SC: %s" % str(body)) @@ -172,13 +193,13 @@ class FRNManagerServer(LineOnlyReceiver): def decodeCC(self, body): # Client login log.msg("CC: %s" % str(body)) self._authOnly() - self.manager.clientLogin(FRNUser(**body)).addCallback( + self.manager.clientLogin(self.serverInfo, FRNUser(**body)).addCallback( self.sendLine) def decodeCD(self, body): # Client logout log.msg("CD: %s" % str(body)) self._authOnly() - self.manager.clientLogout(FRNUser(**body)).addCallback( + self.manager.clientLogout(self.serverInfo, FRNUser(**body)).addCallback( self.sendLine) def decodeIG(self, body): # Account creation diff --git a/frn/protocol/server.py b/frn/protocol/server.py index 80834c7..62d141d 100644 --- a/frn/protocol/server.py +++ b/frn/protocol/server.py @@ -32,20 +32,19 @@ class FRNServer(BufferingLineReceiver, TimeoutMixin): def connectionLost(self, reason): log.msg("Client disconnected: %s" % self.clientAddress.host) - self.stopPinging() - if self.user is not None: - if self.user.ID: - log.msg("Logging out client %s" % self.user.ID) - self.factory.manager.clientLogout(self.user) - self.factory.tracker.logout(self) + self.disconnect() BufferingLineReceiver.connectionLost(self, reason) + def timeoutConnection(self): + log.msg("Client dead: disconnecting %s" % self.user) + self.disconnect() + def lineReceived(self, line): self.resetTimeout() sline = line.strip() if self.waitingKey: if responseToChallange(self.kp) != sline: - self.transport.loseConnection() + self.disconnect() return else: self.waitingKey = False @@ -59,7 +58,7 @@ class FRNServer(BufferingLineReceiver, TimeoutMixin): self.sendAccessFlags(ac,tx) self.sendAccessList(self.factory.tracker.getAcl(self.user.NT)) if self.role not in ['OK', 'ADMIN', 'OWNER']: - self.transport.loseConnection() + self.disconnect() return self.startPinging() self.setTimeout(10.0) @@ -72,7 +71,7 @@ class FRNServer(BufferingLineReceiver, TimeoutMixin): else: command, body = sline.split(':', 1) if command != 'CT' and self.user is None: - self.transport.loseConnection() + self.disconnect() return handler = getattr(self, 'decode'+command, None) if body[0] == '<' and body[-1] == '>': @@ -110,7 +109,12 @@ class FRNServer(BufferingLineReceiver, TimeoutMixin): return succeed(("BLOCK", "")) def disconnect(self): - self.transport.loseConnection() + self.stopPinging() + if self.user is not None: + log.msg("Logging out client %s" % self.user) + self.factory.manager.clientLogout(self.user) + self.factory.tracker.logout(self) + self.transport.loseConnection() def decodeCT(self, body): def authReturned(result): @@ -135,7 +139,7 @@ class FRNServer(BufferingLineReceiver, TimeoutMixin): self.sendLine( self.factory.serverAuth.asXML('MT','SV','AL','BN','BP','KP')) if self.role not in ['OK', 'OWNER', 'ADMIN']: - self.transport.loseConnection() + self.disconnect() else: self.sendNetworkList(self.factory.tracker.getNetworkList()) self.transport.setTcpNoDelay(True) @@ -307,14 +311,14 @@ class FRNServer(BufferingLineReceiver, TimeoutMixin): log.msg("Sending ACL to %s: %s" % (self.user.ON, str(clients))) self.transport.write(chr(7)) self.sendLine(str(len(clients))) - for c in clients: # FIXME + for c in clients: self.sendLine(c.asXML('AI','NN','CT','BC','ON','ID')) def sendAccessFlags(self, access, talk): log.msg("Sending ACL flags to %s: %s" % (self.user.ON, str((access, talk)))) FV = {True: '1', False: 'o'} self.transport.write(chr(10)) - self.sendLine('2') # TODO + self.sendLine('2') self.sendLine(FV[access]) self.sendLine(FV[talk]) diff --git a/frn/userstore/__init__.py b/frn/userstore/__init__.py deleted file mode 100644 index ea81c17..0000000 --- a/frn/userstore/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2010 Maurizio Porrato -# See LICENSE.txt for copyright info - -class FRNUserNotFound(StandardError): pass - - -# vim: set et ai sw=4 ts=4 sts=4: diff --git a/frn/userstore/database.py b/frn/userstore/database.py deleted file mode 100644 index 9966bcc..0000000 --- a/frn/userstore/database.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright 2010 Maurizio Porrato -# See LICENSE.txt for copyright info - -from twisted.python import log -from twisted.enterprise.adbapi import safe -from frn.userstore import FRNUserNotFound -from frn.user import FRNUser - - -def _asDict(curs, *args, **kw): - log.msg("Running query %s %s" % (str(args), str(kw))) - curs.execute(*args, **kw) - result = curs.fetchall() - columns = [d[0] for d in curs.description] - d = [dict(zip(columns, r)) for r in result] - log.msg("Query returned: %s" % str(d)) - return d - - -def _returnId(curs, *args, **kw): - log.msg("Running query %s %s" % (str(args), str(kw))) - curs.execute(*args, **kw) - rowId = curs.lastrowid - log.msg("Query returned: %s" % str(rowId)) - return rowId - - -class DatabaseUserStore(object): - - def __init__(self, pool): - self._pool = pool - - def _query(self, *args, **kw): - return self._pool.runInteraction(_asDict, *args, **kw) - - def getByName(self, name, email): - try: - return self._query(""" - SELECT * FROM frn_users - WHERE "on"=? AND "ea"=?""", - (name, email)).addCallback( - lambda x: FRNUser(**x[0])) - except IndexError: - raise FRNUserNotFound - - def getById(self, userId): - return self._query(""" - SELECT * FROM frn_users - WHERE "id"=?""", - (int(userId), )).addCallback( - lambda x: FRNUser(x[0])) - - def update(self, userId, **kw): - assignments = ','.join(["\"%s\"='%s'" % (k,safe(v)) - for k,v in kw.items()]) - op = "UPDATE frn_users SET "+assignments+" WHERE \"id\"=?" - self._pool.runOperation(op, (int(userId),)) - - def create(self, user): - return self._query( - """INSERT INTO frn_users ("on","ea") VALUES (?,?)""", - (user.ON, user.EA)).addCallback(lambda x: "%015s" % x) - - def list(self): - return self._query("SELECT * FROM frn_users").addCallback( - lambda x: [FRNUser(y) for y in x]) - - def login(self, user): - def gotUser(u): - log.msg("Got user %s" % u.ID) - if u.PW == user.PW: - return u.ID - return "WRONG" - log.msg("Authenticating %s (%s)" % (user.ON, user.EA)) - return self.getByName(user.ON, user.EA).addCallbacks( - gotUser, lambda x: "WRONG") - - def logout(self, userId): - pass - - -# vim: set et ai sw=4 ts=4 sts=4: diff --git a/frn/utility.py b/frn/utility.py index 4dd9b5d..e998c85 100644 --- a/frn/utility.py +++ b/frn/utility.py @@ -3,7 +3,7 @@ # Copyright 2010 Maurizio Porrato # See LICENSE.txt for copyright info -from HTMLParser import HTMLParser +import re from random import choice @@ -19,45 +19,10 @@ def makeRandomChallange(): return ''.join([choice('0123456789') for i in range(6)]) -class SimpleXMLParser(HTMLParser): - """Dirty FRN-specific hack to handle bogus one-level nesting only - XML-like syntax""" - - def handle_starttag(self, tag, attrs): - if not hasattr(self, 'fields'): - self.fields = {} - self.next_field = None - self.next_data = '' - if self.next_field is None: - self.next_field = tag - self.next_data = '' - else: - if attrs: - a = ' '+' '.join(['%s="%s"' % (k,v) for k,v in attrs]) - else: - a = '' - self.next_data += "<%s%s>" % (tag,a) - - def handle_data(self, data): - if self.next_field is not None: - self.next_data += data - - def handle_endtag(self, tag): - if tag == self.next_field: - self.fields[self.next_field] = self.next_data - self.next_field = None - self.next_data = '' - else: - self.next_data += "" % tag - - def get_fields(self): - return self.fields - +re_tag = re.compile(r"<([A-Z]{2})>(.*)") def parseSimpleXML(xml): - p = SimpleXMLParser() - p.feed(xml) - return p.get_fields() + return dict(re_tag.findall(xml)) def formatSimpleXML(elements): diff --git a/manager.py b/manager.py index a4dee64..5d4b6c4 100755 --- a/manager.py +++ b/manager.py @@ -9,25 +9,31 @@ from frn.protocol.manager import FRNManagerServer, FRNManagerServerFactory from twisted.enterprise.adbapi import ConnectionPool from frn.manager.dummy import DummyManager from frn.manager.remote import RemoteManager +from frn.manager.database import DatabaseManager from frn.user import FRNUser from twisted.python import log if __name__ == '__main__': - import sys + import sys - log.startLogging(sys.stderr) + log.startLogging(sys.stderr) - def standardManagerFactory(): - log.msg("Building Manager") - return RemoteManager(reactor) + def dummyManagerFactory(): + log.msg("Building DummyManager") + return DummyManager() - reactor.listenTCP(10025, FRNManagerServerFactory( -# DatabaseUserStore( -# ConnectionPool("sqlite3", "frn_users.sqlite3", -# check_same_thread=False)), -# DummyManager() - standardManagerFactory - )) - reactor.run() + def remoteManagerFactory(): + log.msg("Building RemoteManager") + return RemoteManager(reactor) + + def databaseManagerFactory(): + log.msg("Building DatabaseManager") + return DatabaseManager( + ConnectionPool("sqlite3", "/tmp/frnmanager.sqlite3", cp_noisy=True)) + + reactor.listenTCP(10025, FRNManagerServerFactory( + databaseManagerFactory + )) + reactor.run() # vim: set et ai sw=4 ts=4 sts=4: