Add DB-based system manager server. New simple XML parser.

This commit is contained in:
Maurizio Porrato 2011-01-30 02:29:31 +01:00
parent 02b1c0209c
commit 214cb07962
10 changed files with 207 additions and 191 deletions

View File

@ -8,16 +8,16 @@ from zope.interface import Interface
class IManager(Interface): class IManager(Interface):
def serverLogin(user): def serverLogin(server):
"""Logs server on""" """Logs server on"""
def serverLogout(user): def serverLogout(server):
"""Logs server out""" """Logs server out"""
def clientLogin(user): def clientLogin(server, user):
"""Logs client in""" """Logs client in"""
def clientLogout(user): def clientLogout(server, user):
"""Logs client out""" """Logs client out"""
def getClientList(): def getClientList():

View File

@ -5,33 +5,146 @@
from zope.interface import implements from zope.interface import implements
from frn.manager import IManager 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): class DatabaseManager(object):
implements(IManager) implements(IManager)
def __init__(self, store): def __init__(self, pool):
self._store = store 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): def serverLogin(self, server):
pass 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): def serverLogout(self, server):
pass 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): def clientLogin(self, server, user):
pass 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): def clientLogout(self, server, user):
pass 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): 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): 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: # vim: set et ai sw=4 ts=4 sts=4:

View File

@ -16,20 +16,20 @@ class DummyManager(object):
def _randId(self): def _randId(self):
return '.'.join([str(randint(1,254)) for i in range(4)]) return '.'.join([str(randint(1,254)) for i in range(4)])
def serverLogin(self, user): def serverLogin(self, server):
return defer.succeed(0) return defer.succeed(0)
def serverLogout(self, user): def serverLogout(self, server):
return defer.succeed(None) return defer.succeed(None)
def clientLogin(self, user): def clientLogin(self, server, user):
return defer.succeed(self._randId()) return defer.succeed(self._randId())
def clientLogout(self, user): def clientLogout(self, server, user):
return defer.succeed('OK') return defer.succeed('OK')
def getClientList(self): def getClientList(self):
return defer.succeed([]) return defer.succeed({})
def registerUser(self, user): def registerUser(self, user):
return defer.succeed('OK') return defer.succeed('OK')

View File

@ -55,23 +55,23 @@ class RemoteManager(object):
def doConnect(self): def doConnect(self):
self.reactor.connectTCP(self.server, self.port, self.factory) self.reactor.connectTCP(self.server, self.port, self.factory)
def serverLogin(self, user): def serverLogin(self, server):
self.factory = CustomManagerClientFactory(user) self.factory = CustomManagerClientFactory(server)
self.doConnect() self.doConnect()
return self.factory.deferred return self.factory.deferred
def serverLogout(self, user): def serverLogout(self, server):
if self.factory.client is not None: 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': if self.maskParrot and user.BC == 'Parrot':
u = user.copy(BC='PC Only') u = user.copy(BC='PC Only')
else: else:
u = user.copy() u = user.copy()
return self.factory.client.sendClientLogin(u) return self.factory.client.sendClientLogin(u)
def clientLogout(self, user): def clientLogout(self, server, user):
if self.maskParrot and user.BC == 'Parrot': if self.maskParrot and user.BC == 'Parrot':
u = user.copy(BC='PC Only') u = user.copy(BC='PC Only')
else: else:

View File

@ -112,12 +112,16 @@ class FRNManagerServer(LineOnlyReceiver):
def connectionMade(self): def connectionMade(self):
log.msg("Manager client connected from %s" % self.transport.getPeer().host) log.msg("Manager client connected from %s" % self.transport.getPeer().host)
self._phase = "CONNECTED" self._phase = "CONNECTED"
self.serverInfo = None
self.kp = makeRandomChallange() self.kp = makeRandomChallange()
def connectionLost(self, reason): def connectionLost(self, reason):
log.msg("Manager client disconnected from %s: %s" % log.msg("Manager client disconnected from %s: %s" %
(self.transport.getPeer().host, reason)) (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): def _authOnly(self):
if self._phase != "AUTHENTICATED": if self._phase != "AUTHENTICATED":
@ -129,6 +133,8 @@ class FRNManagerServer(LineOnlyReceiver):
if self._phase == "CHALLANGE": if self._phase == "CHALLANGE":
if sline == responseToChallange(self.kp): if sline == responseToChallange(self.kp):
self._phase = "AUTHENTICATED" self._phase = "AUTHENTICATED"
self.serverInfo = self.tmpServerInfo
log.msg("Auth success: %s" % repr(self.serverInfo))
else: else:
self.transport.loseConnection() self.transport.loseConnection()
else: else:
@ -146,8 +152,19 @@ class FRNManagerServer(LineOnlyReceiver):
handler(xbody) handler(xbody)
def sendClientList(self): 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") log.msg("SM")
self.sendLine('0') # TODO return self.manager.getClientList().addCallback(gotClientList)
def unimplemented(self, cmd, body): def unimplemented(self, cmd, body):
log.err("Unimplemented command %s: %s" % (cmd, str(body))) log.err("Unimplemented command %s: %s" % (cmd, str(body)))
@ -156,13 +173,17 @@ class FRNManagerServer(LineOnlyReceiver):
def sendManagerInfo(res): def sendManagerInfo(res):
if versions.manager > 2009004: if versions.manager > 2009004:
self._phase = "CHALLANGE" self._phase = "CHALLANGE"
self.tmpServerInfo = FRNUser(**body)
else: else:
self._phase = "AUTHENTICATED" self._phase = "AUTHENTICATED"
self.serverInfo = FRNUser(**body)
log.msg("Auth success: %s" % repr(self.serverInfo))
self.sendLine(formatSimpleXML([ self.sendLine(formatSimpleXML([
('SV', versions.server), ('SV', versions.server),
('CV', versions.client), ('CV', versions.client),
('MC', versions.manager), ('MC', versions.manager),
('AL', res['al']), # ('AL', res['al']),
('AL', res),
('KP', self.kp) ('KP', self.kp)
])) ]))
log.msg("SC: %s" % str(body)) log.msg("SC: %s" % str(body))
@ -172,13 +193,13 @@ class FRNManagerServer(LineOnlyReceiver):
def decodeCC(self, body): # Client login def decodeCC(self, body): # Client login
log.msg("CC: %s" % str(body)) log.msg("CC: %s" % str(body))
self._authOnly() self._authOnly()
self.manager.clientLogin(FRNUser(**body)).addCallback( self.manager.clientLogin(self.serverInfo, FRNUser(**body)).addCallback(
self.sendLine) self.sendLine)
def decodeCD(self, body): # Client logout def decodeCD(self, body): # Client logout
log.msg("CD: %s" % str(body)) log.msg("CD: %s" % str(body))
self._authOnly() self._authOnly()
self.manager.clientLogout(FRNUser(**body)).addCallback( self.manager.clientLogout(self.serverInfo, FRNUser(**body)).addCallback(
self.sendLine) self.sendLine)
def decodeIG(self, body): # Account creation def decodeIG(self, body): # Account creation

View File

@ -32,20 +32,19 @@ class FRNServer(BufferingLineReceiver, TimeoutMixin):
def connectionLost(self, reason): def connectionLost(self, reason):
log.msg("Client disconnected: %s" % self.clientAddress.host) log.msg("Client disconnected: %s" % self.clientAddress.host)
self.stopPinging() self.disconnect()
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)
BufferingLineReceiver.connectionLost(self, reason) BufferingLineReceiver.connectionLost(self, reason)
def timeoutConnection(self):
log.msg("Client dead: disconnecting %s" % self.user)
self.disconnect()
def lineReceived(self, line): def lineReceived(self, line):
self.resetTimeout() self.resetTimeout()
sline = line.strip() sline = line.strip()
if self.waitingKey: if self.waitingKey:
if responseToChallange(self.kp) != sline: if responseToChallange(self.kp) != sline:
self.transport.loseConnection() self.disconnect()
return return
else: else:
self.waitingKey = False self.waitingKey = False
@ -59,7 +58,7 @@ class FRNServer(BufferingLineReceiver, TimeoutMixin):
self.sendAccessFlags(ac,tx) self.sendAccessFlags(ac,tx)
self.sendAccessList(self.factory.tracker.getAcl(self.user.NT)) self.sendAccessList(self.factory.tracker.getAcl(self.user.NT))
if self.role not in ['OK', 'ADMIN', 'OWNER']: if self.role not in ['OK', 'ADMIN', 'OWNER']:
self.transport.loseConnection() self.disconnect()
return return
self.startPinging() self.startPinging()
self.setTimeout(10.0) self.setTimeout(10.0)
@ -72,7 +71,7 @@ class FRNServer(BufferingLineReceiver, TimeoutMixin):
else: else:
command, body = sline.split(':', 1) command, body = sline.split(':', 1)
if command != 'CT' and self.user is None: if command != 'CT' and self.user is None:
self.transport.loseConnection() self.disconnect()
return return
handler = getattr(self, 'decode'+command, None) handler = getattr(self, 'decode'+command, None)
if body[0] == '<' and body[-1] == '>': if body[0] == '<' and body[-1] == '>':
@ -110,7 +109,12 @@ class FRNServer(BufferingLineReceiver, TimeoutMixin):
return succeed(("BLOCK", "")) return succeed(("BLOCK", ""))
def disconnect(self): 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 decodeCT(self, body):
def authReturned(result): def authReturned(result):
@ -135,7 +139,7 @@ class FRNServer(BufferingLineReceiver, TimeoutMixin):
self.sendLine( self.sendLine(
self.factory.serverAuth.asXML('MT','SV','AL','BN','BP','KP')) self.factory.serverAuth.asXML('MT','SV','AL','BN','BP','KP'))
if self.role not in ['OK', 'OWNER', 'ADMIN']: if self.role not in ['OK', 'OWNER', 'ADMIN']:
self.transport.loseConnection() self.disconnect()
else: else:
self.sendNetworkList(self.factory.tracker.getNetworkList()) self.sendNetworkList(self.factory.tracker.getNetworkList())
self.transport.setTcpNoDelay(True) self.transport.setTcpNoDelay(True)
@ -307,14 +311,14 @@ class FRNServer(BufferingLineReceiver, TimeoutMixin):
log.msg("Sending ACL to %s: %s" % (self.user.ON, str(clients))) log.msg("Sending ACL to %s: %s" % (self.user.ON, str(clients)))
self.transport.write(chr(7)) self.transport.write(chr(7))
self.sendLine(str(len(clients))) self.sendLine(str(len(clients)))
for c in clients: # FIXME for c in clients:
self.sendLine(c.asXML('AI','NN','CT','BC','ON','ID')) self.sendLine(c.asXML('AI','NN','CT','BC','ON','ID'))
def sendAccessFlags(self, access, talk): def sendAccessFlags(self, access, talk):
log.msg("Sending ACL flags to %s: %s" % (self.user.ON, str((access, talk)))) log.msg("Sending ACL flags to %s: %s" % (self.user.ON, str((access, talk))))
FV = {True: '1', False: 'o'} FV = {True: '1', False: 'o'}
self.transport.write(chr(10)) self.transport.write(chr(10))
self.sendLine('2') # TODO self.sendLine('2')
self.sendLine(FV[access]) self.sendLine(FV[access])
self.sendLine(FV[talk]) self.sendLine(FV[talk])

View File

@ -1,9 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
# See LICENSE.txt for copyright info
class FRNUserNotFound(StandardError): pass
# vim: set et ai sw=4 ts=4 sts=4:

View File

@ -1,84 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
# 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:

View File

@ -3,7 +3,7 @@
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com> # Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
# See LICENSE.txt for copyright info # See LICENSE.txt for copyright info
from HTMLParser import HTMLParser import re
from random import choice from random import choice
@ -19,45 +19,10 @@ def makeRandomChallange():
return ''.join([choice('0123456789') for i in range(6)]) return ''.join([choice('0123456789') for i in range(6)])
class SimpleXMLParser(HTMLParser): re_tag = re.compile(r"<([A-Z]{2})>(.*)</\1>")
"""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 += "</%s>" % tag
def get_fields(self):
return self.fields
def parseSimpleXML(xml): def parseSimpleXML(xml):
p = SimpleXMLParser() return dict(re_tag.findall(xml))
p.feed(xml)
return p.get_fields()
def formatSimpleXML(elements): def formatSimpleXML(elements):

View File

@ -9,25 +9,31 @@ from frn.protocol.manager import FRNManagerServer, FRNManagerServerFactory
from twisted.enterprise.adbapi import ConnectionPool from twisted.enterprise.adbapi import ConnectionPool
from frn.manager.dummy import DummyManager from frn.manager.dummy import DummyManager
from frn.manager.remote import RemoteManager from frn.manager.remote import RemoteManager
from frn.manager.database import DatabaseManager
from frn.user import FRNUser from frn.user import FRNUser
from twisted.python import log from twisted.python import log
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
log.startLogging(sys.stderr) log.startLogging(sys.stderr)
def standardManagerFactory(): def dummyManagerFactory():
log.msg("Building Manager") log.msg("Building DummyManager")
return RemoteManager(reactor) return DummyManager()
reactor.listenTCP(10025, FRNManagerServerFactory( def remoteManagerFactory():
# DatabaseUserStore( log.msg("Building RemoteManager")
# ConnectionPool("sqlite3", "frn_users.sqlite3", return RemoteManager(reactor)
# check_same_thread=False)),
# DummyManager() def databaseManagerFactory():
standardManagerFactory log.msg("Building DatabaseManager")
)) return DatabaseManager(
reactor.run() 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: # vim: set et ai sw=4 ts=4 sts=4: