Fix bad commit
This commit is contained in:
parent
1a19977790
commit
d4a1a0da99
|
@ -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:
|
|
@ -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:<VX>2010002</VX><EA>user@example.com</EA><PW>XYZJKWXY</PW><ON>CALLSIGN, Name</ON><BC>PC Only</BC><DS></DS><NN>Country</NN><CT>City - Locator</CT><NT>Network</NT>
|
||||
|
||||
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
|
||||
<MT></MT><SV>2009005</SV><AL>OK</AL><BN>backup.server.org</BN><BP>10024</BP><KP>327119</KP>
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
|
||||
# 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:
|
|
@ -0,0 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
|
||||
# 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:
|
|
@ -0,0 +1,38 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
|
||||
# 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:
|
|
@ -0,0 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
|
||||
# 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:
|
|
@ -0,0 +1,40 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
|
||||
# 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:
|
|
@ -0,0 +1,197 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
|
||||
# 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:
|
|
@ -0,0 +1,288 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
|
||||
# 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:
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
|
||||
# See LICENSE.txt for copyright info
|
||||
|
||||
client = 2010002
|
||||
server = 2009005
|
||||
manager = 2009005
|
||||
|
||||
# vim: set et ai sw=4 ts=4 sts=4:
|
|
@ -0,0 +1,62 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
|
||||
# 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:
|
|
@ -0,0 +1,9 @@
|
|||
# -*- 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:
|
|
@ -0,0 +1,84 @@
|
|||
# -*- 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:
|
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
|
||||
# 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:
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
|
||||
# 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:
|
Loading…
Reference in New Issue