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: