From d4a1a0da99cbd3799a7195013ea28da0c08cc8d6 Mon Sep 17 00:00:00 2001 From: Maurizio Porrato Date: Wed, 18 Aug 2010 17:46:58 +0200 Subject: [PATCH] Fix bad commit --- docs/frn_users.sql | 23 +++ docs/protocols.markdown | 118 ++++++++++++++++ frn/manager/__init__.py | 30 ++++ frn/manager/database.py | 37 +++++ frn/manager/dummy.py | 38 +++++ frn/manager/remote.py | 50 +++++++ frn/protocol/common.py | 40 ++++++ frn/protocol/manager.py | 197 ++++++++++++++++++++++++++ frn/protocol/server.py | 288 ++++++++++++++++++++++++++++++++++++++ frn/protocol/versions.py | 10 ++ frn/user.py | 62 ++++++++ frn/userstore/__init__.py | 9 ++ frn/userstore/database.py | 84 +++++++++++ manager.py | 33 +++++ server.py | 58 ++++++++ 15 files changed, 1077 insertions(+) create mode 100644 docs/frn_users.sql create mode 100644 docs/protocols.markdown create mode 100644 frn/manager/__init__.py create mode 100644 frn/manager/database.py create mode 100644 frn/manager/dummy.py create mode 100644 frn/manager/remote.py create mode 100644 frn/protocol/common.py create mode 100644 frn/protocol/manager.py create mode 100644 frn/protocol/server.py create mode 100644 frn/protocol/versions.py create mode 100644 frn/user.py create mode 100644 frn/userstore/__init__.py create mode 100644 frn/userstore/database.py create mode 100755 manager.py create mode 100755 server.py diff --git a/docs/frn_users.sql b/docs/frn_users.sql new file mode 100644 index 0000000..ea20ac5 --- /dev/null +++ b/docs/frn_users.sql @@ -0,0 +1,23 @@ +CREATE TABLE frn_users ( +-- Fields required for FRN compatibility + "id" INTEGER PRIMARY KEY AUTOINCREMENT, -- User ID + "on" VARCHAR(30), -- Operator name (Callsign, Name) + "ea" VARCHAR(30), -- Email address + "pw" VARCHAR(30), -- Password + "bc" VARCHAR(30), -- Band/channel + "ds" VARCHAR(30), -- Description + "ct" VARCHAR(30), -- City - Locator + "nn" VARCHAR(30), -- Country + "nt" VARCHAR(30), -- Network + "ip" VARCHAR(15), -- Client IP +-- GRN specific fields + "rip" VARCHAR(20), -- IP the account was registered from + "cre" TIMESTAMP NOT NULL -- Date/time of account creation + DEFAULT CURRENT_TIMESTAMP, + "act" TIMESTAMP, -- Date/time of last account activity + "srv" VARCHAR(30) -- Server:port where usere is actually logged +); + +CREATE UNIQUE INDEX idx_frn_users_on_ea ON frn_users("on","ea"); + +-- vim: set et ai sw=4 ts=4 sts=4: diff --git a/docs/protocols.markdown b/docs/protocols.markdown new file mode 100644 index 0000000..20fb0e4 --- /dev/null +++ b/docs/protocols.markdown @@ -0,0 +1,118 @@ +The FreeRadioNetwork protocol +============================= + +This document describes the results of reverse-engeneering the official +set of [FreeRadioNetwork](http//freeradionetwork.eu) programs through +network traffic sniffing and software decompilation. + + +System components +----------------- + +The FreeRadioNetwork system is composed of three programs: + +* client +* server +* system manager + +Client and server executables are freely available, while the system +manager is not. The role of the system manager is account management, +including password generation and credentials checking, keeping track +of active servers and connected users. + +The client talks to the system manager only to get the list of active +servers and to request the creation of a new account/password: any other +communication from the client goes to the server. + +Server listens for clients connection on TCP port 10024 (by default, but +can be changed through the server GUI), while the system manager listen +for server connections and client queries on TCP port 10025. In both the +client and the server, the system manager address is hard coded as +`frn.no-ip.info`. + + +Client - server protocol +------------------------ + +The client sends requests to the server in the form of CR+LF terminated +ascii text lines. Response to commands and unsolicited events from the +server (i.e. incoming text messages), excluding authentication procedure +and voice packets, starts with a byte indicating the message type +followed by one CR+LF terminated ascii line containing the decimal count +of lines following and then the lines composing the message body. + + +### Authentication ### + +As soon as the TCP connection is established, the client sends a line +containing user's info and authentication credential formatted enclosing +fields between html-like tags, for example: + + CT:2010002user@example.comXYZJKWXYCALLSIGN, NamePC OnlyCountryCity - LocatorNetwork + +The line is, as usual, followed by the CR and LF chars (ascii 13 an 10). +The meaning of the fields is the following: + +* **VX**: Client version (four digits for the year and three digits for +the release number) +* **EA**: User's email address +* **PW**: The password assigned by the system manager to the user +* **ON**: Operator's name, usually in the form CALLSIGN, RealName +* **BC**: Indicates the client type: + * `PC Only` + * `Parrot` + * `Crosslink` + * *BBB* `Ch`*CCC* *MM* `CTC`*TT* where **BBB** is the band (3 digits: + 433, 446, 027,...), **CCC** is the channel number (3 digits), **MM** + is the modulation (AM or FM), **TT** is the subtone number (2 digits) +* **DS**: Description, only used for gateways, empty for other client +types +* **NN**: Country +* **CT**: Client's location in the form of City - Locator +* **NT**: Network (channel, room) to join on login + +In response to the authentication request, the server sends two lines of +text (CR+LF terminated): + + 2010002 + 2009005OKbackup.server.org10024327119 + +The first line contains the latest client version available. +The second one contains authentication results and server info: + +* **MT**: Unknown meaning, always empty +* **SV**: Server version +* **AL**: Authentication result: + * `OK`: Success, unprivileged user + * `ADMIN`: Success, user has administration privileges + * `OWNER`: Success, user is the server owner + * `NOK`: Failed, invalid client version + * `WRONG`: Failed, invalid credentials + * `BLOCK`: Failed, user already logged in +* **BN**: Backup server +* **BP**: Backup server port +* **KP**: Six digits code used for second phase + +At this point, if authentication succeeds, server versions greater than +2009004 needs one more line from the client before considering the +connection fully established. The line is a five digits number computed +as follows: + +1. Split the KP code in two-digits numbers: `AABBCC` +2. Let `X` be the result of: `(AA+2)*(BB+1)+(CC+4)*(CC+7)` +3. Extend `X` to be 5 digits long, padding to the left with zeroes as +needed and let's call the resulting digits `DEFGH` respectively +4. The authentication code is composed by rearranging the digits as +`GDFHE` + +For example, let's KP be `327119`. AA would be 32, BB=71 and CC=19. + + X = (32+2)*(71+1)+(19+4)*(19+7) = 3046 + +Padding to five digits we would get `03046`, so D=0, E=3, F=0, G=4, H=6 +so the auth code wold be `40063`. + + + + + diff --git a/frn/manager/__init__.py b/frn/manager/__init__.py new file mode 100644 index 0000000..29de6ab --- /dev/null +++ b/frn/manager/__init__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# See LICENSE.txt for copyright info + +from zope.interfaces import Interface + + +class IManager(Interface): + + def serverLogin(user): + """Logs server on""" + + def serverLogout(user): + """Logs server out""" + + def clientLogin(user): + """Logs client in""" + + def clientLogout(user): + """Logs client out""" + + def getClientList(): + """Lists logged in clients""" + + def registerUser(user): + """Registers new user""" + + +# vim: set et ai sw=4 ts=4 sts=4: diff --git a/frn/manager/database.py b/frn/manager/database.py new file mode 100644 index 0000000..74cf93c --- /dev/null +++ b/frn/manager/database.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# See LICENSE.txt for copyright info + +from zope.interfaces import implements +from frn.manager import IManager +from frn.userstore.database import DatabaseUserStore + + +class DatabaseManager(object): + + implements(IManager) + + def __init__(self, store): + self._store = store + + def serverLogin(self, user): + pass + + def serverLogout(self, user): + pass + + def clientLogin(self, user): + pass + + def clientLogout(self, user): + pass + + def getClientList(self): + pass + + def registerUser(self, user): + pass + + +# vim: set et ai sw=4 ts=4 sts=4: diff --git a/frn/manager/dummy.py b/frn/manager/dummy.py new file mode 100644 index 0000000..703b7ee --- /dev/null +++ b/frn/manager/dummy.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# See LICENSE.txt for copyright info + +from zope.interfaces import implements +from frn.manager import IManager +from twisted.internet import defer +from random import randint + + +class DummyManager(object): + + implements(IManager) + + def _randId(self): + return '.'.join([str(randint(1,254)) for i in range(4)]) + + def serverLogin(self, user): + return defer.succeed(0) + + def serverLogout(self, user): + return defer.succeed(None) + + def clientLogin(self, user): + return defer.succeed(self._randId()) + + def clientLogout(self, user): + return defer.succeed('OK') + + def getClientList(self): + return defer.succeed([]) + + def registerUser(self, user): + return defer.succeed('OK') + + +# vim: set et ai sw=4 ts=4 sts=4: diff --git a/frn/manager/remote.py b/frn/manager/remote.py new file mode 100644 index 0000000..bde7eb0 --- /dev/null +++ b/frn/manager/remote.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# See LICENSE.txt for copyright info + +from zope.interfaces import implements +from frn.manager import IManager +from twisted.python import log +from frn.protocol.manager import FRNManagerClient, FRNManagerClientFactory + + +class RemoteManager(object): + + implements(IManager) + + def __init__(self, reactor, server='frn.no-ip.info', port=10025): + self.reactor = reactor + self.server = server + self.port = port + self.factory = FRNManagerClientFactory() + self.factory.continueTrying = 0 # FIXME + + def serverLogin(self, user): + def connectionDone(conn): + log.msg("%s connected" % self.server) + self.managerConnection = conn + return conn + self.reactor.connectTCP(self.server, self.port, self.factory) + log.msg("RemoteManager started connecting %s" % self.server) + return self.factory.managerConnection.addCallback( + connectionDone).addCallback( + lambda _: self.managerConnection.sendServerLogin(user)) + + def serverLogout(self, user): + return self.managerConnection.sendServerLogout(user) + + def clientLogin(self, user): + return self.managerConnection.sendClientLogin(user) + + def clientLogout(self, user): + return self.managerConnection.sendClientLogout(user) + + def getClientList(self): + return self.managerConnection.getClientList() + + def registerUser(self, user): + return self.managerConnection.registerUser(user) + + +# vim: set et ai sw=4 ts=4 sts=4: diff --git a/frn/protocol/common.py b/frn/protocol/common.py new file mode 100644 index 0000000..95bf8fd --- /dev/null +++ b/frn/protocol/common.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# See LICENSE.txt for copyright info + +from twisted.protocols.basic import LineReceiver + + +class BufferingLineReceiver(LineReceiver): + + def connectionMade(self): + self.rawBuffer = "" + self.rawExpected = None + LineReceiver.connectionMade(self) + + def expectRawData(self, howmany): + self.setRawMode() + self.rawExpected = howmany + self.rawDataReceived('') + + def rawDataReceived(self, data): + if self.rawExpected is None: + raise NotImplementedError + self.rawBuffer += data + if len(self.rawBuffer) >= self.rawExpected: + expected = self.rawBuffer[:self.rawExpected] + rest = self.rawBuffer[self.rawExpected:] + self.rawBuffer = "" + self.rawExpected = None + self.expectedReceived(expected) + self.dataReceived(rest) + + def setLineMode(self, extra=""): + LineReceiver.setLineMode(self, extra+self.rawBuffer) + + def expectedReceived(self, data): + raise NotImplementedError + + +# vim: set et ai sw=4 ts=4 sts=4: diff --git a/frn/protocol/manager.py b/frn/protocol/manager.py new file mode 100644 index 0000000..ca967d9 --- /dev/null +++ b/frn/protocol/manager.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# See LICENSE.txt for copyright info + +from twisted.internet.defer import Deferred, succeed +from twisted.internet.protocol import ReconnectingClientFactory, ServerFactory +from twisted.protocols.basic import LineOnlyReceiver +from twisted.internet.task import LoopingCall +from twisted.python import log +from frn.user import FRNUser +from frn.protocol import versions +from frn.utility import * + + +class FRNManagerClient(LineOnlyReceiver): + + def connectionMade(self): + log.msg("Connected to manager [%s]" % self.transport.getPeer().host) + self.notifications = [] + if not self.factory.managerConnection.called: # FIXME: Why??? + self.factory.managerConnection.callback(self) + + def connectionLost(self, reason): + log.msg("Manager disconnected") + try: + self.pingtimer.stop() + except: pass + for d in self.notifications: + d.errback(reason) + self.factory.managerConnection = Deferred() + + def notifyFinish(self): + self.notifications.append(Deferred()) + return self.notifications[-1] + + def finish(self, result): + d = self.notifications[0] + del self.notifications[0] + d.callback(result) + + def lineReceived(self, line): + log.msg("notifications: %s" % str(self.notifications)) + if hasattr(self, 'serverlist'): + # TODO + pass + else: + self.finish(line.strip()) + + def sendServerLogin(self, user): + def loginDone(result): + self.managerdata = parseSimpleXML(result) + log.msg("Server login succeeded: %s" % str(self.managerdata)) + if int(self.managerdata['mc']) > 2009004: + self.sendLine(responseToChallange( + self.managerdata['kp'])) + self.pingtimer = LoopingCall(self.sendPing) + self.pingtimer.start(3.0, False) + self.factory.resetDelay() + return self.managerdata + + log.msg("Sending server login") + user.VX = versions.server + self.sendLine("SC:"+user.asXML( + 'VX','SN','PT','OW','PW')) + return self.notifyFinish().addCallback(loginDone) + + def sendServerLogout(self, user): + self.transport.loseConnection() + return succeed(None) + + def sendPing(self): + self.sendLine("P") + return self.notifyFinish() + + def sendClientLogin(self, client): + self.sendLine("CC:"+client.asXML( + 'EA','PW','ON','BC','NN','CT','NT','DS','IP')) + return self.notifyFinish() + + def sendClientLogout(self, client): + self.sendLine("CD:"+client.asXML('ID')) + return self.notifyFinish() + + def getClientList(self): + #self.sendLine('SM') + raise NotImplementedError # TODO + + def sendRegisterUser(self, user): + self.sendLine("IG:"+user.asXML( + 'ON','EA','BC','DS','NN','CT')) + return self.notifyFinish() + + +class FRNManagerClientFactory(ReconnectingClientFactory): + + protocol = FRNManagerClient + + def startFactory(self): + self.managerConnection = Deferred() + ReconnectingClientFactory.startFactory(self) + + +class FRNManagerServer(LineOnlyReceiver): + + def connectionMade(self): + log.msg("Manager client connected from %s" % self.transport.getPeer().host) + self._phase = "CONNECTED" + 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 + + def _authOnly(self): + if self._phase != "AUTHENTICATED": + log.msg("Unauthorized action!") + self.transport.loseConnection() + + def lineReceived(self, line): + sline = line.strip() + if self._phase == "CHALLANGE": + if sline == responseToChallange(self.kp): + self._phase = "AUTHENTICATED" + else: + self.transport.loseConnection() + else: + if sline == 'P': # Ping + self.sendLine('F') + elif sline == 'SM': # Client list + self.sendClientList() + else: + cmd, body = sline.split(':', 1) + xbody = parseSimpleXML(body) + handler = getattr(self, 'decode'+cmd, None) + if handler is None: + self.unimplemented(cmd, xbody) + else: + handler(xbody) + + def sendClientList(self): + log.msg("SM") + self.sendLine('0') # TODO + + def unimplemented(self, cmd, body): + log.err("Unimplemented command %s: %s" % (cmd, str(body))) + + def decodeSC(self, body): # Server login + def sendManagerInfo(res): + if versions.manager > 2009004: + self._phase = "CHALLANGE" + else: + self._phase = "AUTHENTICATED" + self.sendLine(formatSimpleXML([ + ('SV', versions.server), + ('CV', versions.client), + ('MC', versions.manager), + ('AL', res['al']), + ('KP', self.kp) + ])) + log.msg("SC: %s" % str(body)) + self.manager.serverLogin(FRNUser(**body)).addCallback( + sendManagerInfo) # TODO: second authentication phase + + def decodeCC(self, body): # Client login + log.msg("CC: %s" % str(body)) + self._authOnly() + self.manager.clientLogin(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.sendLine) + + def decodeIG(self, body): # Account creation + log.msg("IG: %s" % str(body)) + self.manager.registerUser(FRNUser(**body)).addCallback( + self.sendLine) + + +class FRNManagerServerFactory(ServerFactory): + + protocol = FRNManagerServer + + def __init__(self, managerFactory): + self.managerFactory = managerFactory + + def buildProtocol(self, addr): + p = ServerFactory.buildProtocol(self, addr) + p.manager = self.managerFactory() + return p + + +# vim: set et ai sw=4 ts=4 sts=4: diff --git a/frn/protocol/server.py b/frn/protocol/server.py new file mode 100644 index 0000000..e898862 --- /dev/null +++ b/frn/protocol/server.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# See LICENSE.txt for copyright info + +from random import choice +from twisted.internet.defer import Deferred +from twisted.internet.protocol import ServerFactory +from twisted.protocols.policies import TimeoutMixin +from twisted.internet.task import LoopingCall +from twisted.python import log +from frn.user import FRNUser +from frn.protocol import versions +from frn.protocol.common import BufferingLineReceiver +from frn.utility import * + + +class FRNServer(BufferingLineReceiver, TimeoutMixin): + + def connectionMade(self): + BufferingLineReceiver.connectionMade(self) + log.msg("Connection from %s" % self.transport.getPeer().host) + self.clientAddress = self.transport.getPeer() + self.user = None + self.role = None + self.kp = makeRandomChallange() + self.waitingKey = False + self.pingTimer = LoopingCall(self.sendPing) + self.pingCount = 0 + self.setTimeout(25.0) + + def connectionLost(self, reason): + log.msg("Client disconnected: %s" % self.clientAddress.host) + try: + self.pingTimer.stop() + except AssertionError: pass + try: + self.factory.clientList.remove(self) + self.factory.manager.clientLogout(self.user) + self.factory.sendClientList([self.user.NT]) + except ValueError: pass + BufferingLineReceiver.connectionLost(self, reason) + + def updateClient(self, **kw): + d = {} + for k, v in [(x.lower(), str(y)) for x,y in kw.items()]: + if self.user.get(k) != v: + d[k] = v + if len(d) > 0: + self.user.update(**d) + self.factory.sendClientList([self.user.NT]) + + def lineReceived(self, line): + self.resetTimeout() + sline = line.strip() + if self.waitingKey: + if responseToChallange(self.kp) != sline: + self.transport.loseConnection() + return + else: + self.waitingKey = False + if self.user.ID in self.factory.muteList: + self.user.M = 1 + else: + self.user.M = 0 + self.user.S = 0 + self.factory.clientList.append(self) + self.factory.sendClientList([self.user.NT]) # FIXME: older servers can't get here + self.sendNetworkList() + if self.role in ['OWNER', 'ADMIN']: + self.sendMuteList() + self.sendBlockList() + if self.role == 'OWNER': + self.sendAccessFlags() + self.sendAccessList() + self.pingCount += 1 + self.pingTimer.start(0.5) + self.setTimeout(10.0) + return + if sline == 'P': # Pong + #log.msg('Pong') + self.pingCount -= 1 + return + if sline in ['RX0', 'TX0', 'TX1']: + command, body = sline[:2], sline[2:] + else: + command, body = sline.split(':', 1) + if command != 'CT' and self.user is None: + self.transport.loseConnection() + return + handler = getattr(self, 'decode'+command, None) + if body[0] == '<' and body[-1] == '>': + body = parseSimpleXML(body) + if handler is not None: + handler(body) + else: + self.unimplemented(command, body) + self.transport.loseConnection() # ??? + + def expectedReceived(self, data): + self.resetTimeout() + self.setLineMode() + self.audioFrameReceived(data) + + def unimplemented(self, command, body): + log.err("Unimplemented message: %s: %s" % (command, body)) + + def authenticate(self, user): + def loginReturned(userId): + if userId != 'WRONG': + return ("OK", userId) # FIXME: return OWNER or ADMIN eventually + else: + return ("WRONG", "") + user.IP = self.clientAddress.host + return self.factory.manager.clientLogin(user).addCallback(loginReturned) + + def decodeCT(self, body): + def authReturned(result): + self.role, clientId = result + log.msg("AUTH result: %s %s" % (self.role, clientId)) + if self.role == 'OK': + self.user = FRNUser(**body) + self.user.ID = clientId + if self.role == 'OK': + if self.user.EA == self.factory.serverAuth.OW: + self.role = 'OWNER' + elif self.user.EA in self.factory.adminList: + self.role = 'ADMIN' + # TODO: Blocklist + if versions.server > 2009004: + self.waitingKey = True + self.sendLine(str(versions.client)) + self.factory.serverAuth.update(MT='', SV=versions.server, + AL=self.role, KP=self.kp) + self.sendLine( + self.factory.serverAuth.asXML('MT','SV','AL','BN','BP','KP')) + if self.role not in ['OK', 'OWNER', 'ADMIN']: + self.transport.loseConnection() + return self.authenticate(FRNUser(**body)).addCallback( + authReturned) + + def decodeRX(self, body): + log.msg("RX%d" % int(body)) + if not self.pingTimer.running: + self.pingTimer.start(0.5) + + def decodeST(self, body): + log.msg("Set status = %d" % int(body)) + self.updateClient(S=int(body)) + + def decodeTM(self, body): + log.msg("TM: %s" % str(body)) + if body['id'] == '': + msgtype = 'A' + else: + msgtype = 'P' + for c in self.factory.clientList: + if msgtype == 'A' or c.user.ID == body['id']: + if c != self: + c.sendTextMessage(self.user.ID, body['ms'], msgtype) + + def decodeTX(self, body): + if body == '0': # FIXME: Mute? + log.msg("TX0") + clientIdx = self.factory.clientList.index(self)+1 + ih,il = divmod(clientIdx, 256) + self.transport.write(chr(1)+chr(ih)+chr(il)) + elif body == '1': + log.msg("TX1") + self.expectRawData(325) + if self.pingTimer.running: + self.pingTimer.stop() + + def audioFrameReceived(self, frame): + log.msg("audioFrameReceived") + clientIdx = self.factory.clientList.index(self)+1 + for c in self.factory.clientList: + if int(c.user.S) < 2 and c != self: + log.msg("Sending to %s" % c.user.ON) + c.sendAudioFrame(clientIdx, frame) + + def sendPing(self): + if self.pingCount > 20: + log.msg("Client %s is dead: disconnecting" % + self.clientAddress.host) + self.transport.loseConnection() +# log.msg(self.pingCount) + self.pingCount += 1 + self.transport.write(chr(0)) + + def sendClientList(self, clients): + self.transport.write(chr(3)+chr(0)+chr(0)) + self.sendLine(str(len(clients))) + for client in clients: + self.sendLine(client.asXML( + 'S','M','NN','CT','BC','ON','ID','DS' + )) + self.pingCount += 1 + + def sendNetworkList(self): + log.msg("Send network list") + self.transport.write(chr(5)) + nets = self.factory.getNetworkList() + self.sendLine(str(len(nets))) + for net in nets: + self.sendLine(net) + self.pingCount += 1 + + def sendMuteList(self): + self.transport.write(chr(9)) + self.sendLine('0') # TODO + + def sendBlockList(self): + self.transport.write(chr(8)) + self.sendLine('0') # TODO + + def sendAdminList(self): + self.transport.write(chr(6)) + self.sendLine('0') # TODO + + def sendAccessList(self): + self.transport.write(chr(7)) + self.sendLine('0') # TODO + + def sendAccessFlags(self): + self.transport.write(chr(10)) + self.sendLine('2') # TODO + self.sendLine('o') + self.sendLine('o') + + def sendTextMessage(self, clientId, message, target): + self.transport.write(chr(4)) + self.sendLine('3') + self.sendLine(clientId) + self.sendLine(message) + self.sendLine(target) + self.pingCount += 1 + + def sendAudioFrame(self, clientIdx, frame): + self.transport.write(chr(2)) + ih,il = divmod(clientIdx, 256) + self.transport.write(chr(ih)+chr(il)) + self.transport.write(frame) + + +class FRNServerFactory(ServerFactory): + + protocol = FRNServer + + def __init__(self, manager, serverAuth): + self.manager = manager + self.serverAuth = serverAuth + self.talking = None + self.clientList = [] + self.adminList = [] + self.muteList = [] + self.blockList = [] + self.officialNets = [] + + def startFactory(self): + ServerFactory.startFactory(self) + self.manager.serverLogin(self.serverAuth) + + def stopFactory(self): + self.manager.serverLogout(self.serverAuth) + ServerFactory.stopFactory(self) + + def getNetworkList(self): + n = set([c.user.NT for c in self.clientList]) + o = set(self.officialNets) + return self.officialNets+list(n-o) + + def sendNetworkList(self): + n = self.getNetworkList() + for c in self.clientList: + c.sendNetworkList(n) + + def sendClientList(self, networks=[]): + n = {} + for c in self.clientList: + l = n.get(c.user.NT, []) + l.append(c.user) + n[c.user.NT] = l + for c in self.clientList: + if (not networks) or (c.user.NT in networks): + c.sendClientList(n[c.user.NT]) + +# vim: set et ai sw=4 ts=4 sts=4: diff --git a/frn/protocol/versions.py b/frn/protocol/versions.py new file mode 100644 index 0000000..f267cce --- /dev/null +++ b/frn/protocol/versions.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# See LICENSE.txt for copyright info + +client = 2010002 +server = 2009005 +manager = 2009005 + +# vim: set et ai sw=4 ts=4 sts=4: diff --git a/frn/user.py b/frn/user.py new file mode 100644 index 0000000..c675be3 --- /dev/null +++ b/frn/user.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# See LICENSE.txt for copyright info + +from frn.utility import parseSimpleXML, formatSimpleXML + + +class FRNUser(object): + + def __init__(self, **kw): + self._fields = {} + self.update(**kw) + + def __getattr__(self, attr): + if attr.startswith('_'): + return super(FRNUser, self).__getattr__(attr) + else: + return self.get(attr) + + def __setattr__(self, attr, value): + if attr.startswith('_'): + super(FRNUser, self).__setattr__(attr, value) + else: + self.set(attr, value) + + def __str__(self): + return asXML(self) + + def __repr__(self): + return "FRNUser(%s)" % \ + ', '.join(["%s='%s'" % (k,v) for k,v in self.items()]) + + def set(self, field, value): + self._fields[field.lower()] = str(value) + + def get(self, field, default=''): + return self._fields[field.lower()] + + def items(self, *fields): + if len(fields) == 0: + fields = self._fields.keys() + r = [] + for field in fields: + r.append((field.upper(), self.get(field))) + return r + + def dict(self, *fields): + return dict(self.items(*fields)) + + def update(self, **kw): + for field, value in kw.items(): + self.set(field, value) + + def updateXML(self, xml): + self.update(dict(parseSimpleXML(xml))) + + def asXML(self, *fields): + return formatSimpleXML(self.items(*fields)) + + +# vim: set et ai sw=4 ts=4 sts=4: diff --git a/frn/userstore/__init__.py b/frn/userstore/__init__.py new file mode 100644 index 0000000..ea81c17 --- /dev/null +++ b/frn/userstore/__init__.py @@ -0,0 +1,9 @@ +# -*- 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 new file mode 100644 index 0000000..9966bcc --- /dev/null +++ b/frn/userstore/database.py @@ -0,0 +1,84 @@ +# -*- 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/manager.py b/manager.py new file mode 100755 index 0000000..d8155cb --- /dev/null +++ b/manager.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# See LICENSE.txt for copyright info + +from twisted.internet import reactor +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.user import FRNUser +from twisted.python import log + +if __name__ == '__main__': + import sys + + log.startLogging(sys.stderr) + + def standardManagerFactory(): + log.msg("Building Manager") + return RemoteManager(reactor, '83.82.28.221') + + reactor.listenTCP(10025, FRNManagerServerFactory( +# DatabaseUserStore( +# ConnectionPool("sqlite3", "frn_users.sqlite3", +# check_same_thread=False)), +# DummyManager() + standardManagerFactory + )) + reactor.run() + +# vim: set et ai sw=4 ts=4 sts=4: diff --git a/server.py b/server.py new file mode 100755 index 0000000..8895a49 --- /dev/null +++ b/server.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# See LICENSE.txt for copyright info + +from twisted.internet import reactor +from frn.protocol.server import FRNServer, FRNServerFactory +from twisted.enterprise.adbapi import ConnectionPool +from frn.manager.remote import RemoteManager +from frn.manager.dummy import DummyManager +from frn.user import FRNUser +from twisted.python import log + +if __name__ == '__main__': + import sys + from os.path import dirname, join as pjoin + from ConfigParser import ConfigParser + + log.startLogging(sys.stderr) + + basedir = dirname(__file__) + + acfg = ConfigParser() + acfg.read(['/etc/grn/accounts.conf', + pjoin(basedir,'accounts.conf'), 'accounts.conf']) + + scfg = ConfigParser() + scfg.read(['/etc/grn/servers.conf', + pjoin(basedir,'servers.conf'), 'servers.conf']) + + argc = len(sys.argv) + + if argc >= 3: + account_name = sys.argv[1] + server_name = sys.argv[2] + + server = scfg.get(server_name, 'server') + port = scfg.getint(server_name, 'port') + backup_server = scfg.get(server_name, 'backup_server') + backup_port = scfg.getint(server_name, 'backup_port') + owner = acfg.get(account_name, 'email') + password = acfg.get(account_name, 'password') + + reactor.listenTCP(10024, FRNServerFactory( + # DatabaseUserStore( + # ConnectionPool("sqlite3", "frn_users.sqlite3", + # check_same_thread=False)), + # RemoteManager(reactor, '83.82.28.221'), + DummyManager(), + FRNUser( + SN=server,PT=port, + BN=backup_server, BP=backup_port, + OW=owner,PW=password) + )) + reactor.run() + +# vim: set et ai sw=4 ts=4 sts=4: