Major features improvements

- Add owner and admin commands support
- Add System Manager client and server protocol implementations
- Start some protocols documentation
- Fixed parrot gsm frames timing issues
This commit is contained in:
Maurizio Porrato 2010-08-18 17:40:19 +02:00
parent abd4301390
commit 1a19977790
4 changed files with 193 additions and 86 deletions

4
.gitignore vendored
View File

@ -1,5 +1,7 @@
*.py[co] *.py[co]
*~ *~
stations.conf accounts.conf
servers.conf
recordings/* recordings/*
sounds/* sounds/*
*.sqlite3

View File

@ -1,66 +1,59 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
#
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
# See LICENSE.txt for copyright info
from Queue import Queue from Queue import Queue
from twisted.internet.protocol import ClientFactory from twisted.internet.protocol import ReconnectingClientFactory
from twisted.protocols.basic import LineReceiver from twisted.protocols.policies import TimeoutMixin
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
from twisted.python import log from twisted.python import log
from frn.protocol import versions
from frn.user import FRNUser
from frn.protocol.common import BufferingLineReceiver
from frn.utility import * from frn.utility import *
class FRNClient(LineReceiver): class FRNClient(BufferingLineReceiver, TimeoutMixin):
client_version = 2010002
def connectionMade(self): def connectionMade(self):
BufferingLineReceiver.connectionMade(self)
self.user.VX = versions.client
self.txq = Queue() self.txq = Queue()
self.setTimeout(25.0)
self.login() self.login()
def ready(self): def ready(self):
self.status = 'READY' self.status = 'READY'
self.msgbuffer = [] self.msgbuffer = []
self.phase = 0 self.phase = 0
self.setRawMode() self.expectRawData(1)
def startMultiLineMessage(self, msgtype, rest=''): def startMultiLineMessage(self, msgtype):
self.status = msgtype self.status = msgtype
self.phase = None self.phase = None
self.setLineMode(rest) self.setLineMode()
def stopMultiLineMessage(self):
handler = getattr(self, 'decode'+self.status, None)
status = self.status
message = self.msgbuffer
self.ready()
if handler is not None:
handler(message)
else:
self.unimplemented(status, message)
def collectMultiLineMessage(self, line): def collectMultiLineMessage(self, line):
if self.phase is None: if self.phase is None:
while line[0] not in '0123456789': # needed for client list
line = line[1:]
self.expected_lines = int(line.strip()) self.expected_lines = int(line.strip())
self.msgbuffer = [] self.msgbuffer = []
self.phase = 0 self.phase = 0
else: else:
self.msgbuffer.append(line) self.msgbuffer.append(line)
self.phase += 1 self.phase += 1
if self.phase >= self.expected_lines: if self.phase >= self.expected_lines:
handler = getattr(self, 'decode'+self.status, self.unimplemented) self.stopMultiLineMessage()
message = self.msgbuffer
self.ready()
handler(message)
def startAudioMessage(self, rest=''):
self.status = 'AUDIO'
self.phase = None
self.msgbuffer = ''
if len(rest) > 0:
self.collectAudioMessage(rest)
def collectAudioMessage(self, data):
needed = min(327, max(0, 327-len(self.msgbuffer)))
if len(data) > 0:
self.msgbuffer += data[:needed]
if len(self.msgbuffer) >= 327:
audio_data = self.msgbuffer
source = ord(audio_data[0])*256+ord(audio_data[1])
self.ready()
self.decodeAUDIO(source, audio_data[2:])
if len(data) > needed:
self.dataReceived(data[needed:])
def lineReceived(self, line): def lineReceived(self, line):
if self.status == 'AUTH': if self.status == 'AUTH':
@ -69,57 +62,60 @@ class FRNClient(LineReceiver):
self.phase = 1 self.phase = 1
else: else:
self.serverdata = parseSimpleXML(line.strip()) self.serverdata = parseSimpleXML(line.strip())
self.loginResponse(self.serverdata)
if int(self.serverdata['sv']) > 2009004: if int(self.serverdata['sv']) > 2009004:
self.sendLine(makeAuthKey(self.serverdata['kp'])) self.sendLine(responseToChallange(
self.serverdata['kp']))
self.ready() self.ready()
self.setTimeout(10.0)
self.factory.resetDelay()
self.loginResponse(self.serverdata)
else: else:
self.collectMultiLineMessage(line) self.collectMultiLineMessage(line)
def rawDataReceived(self, data): def expectedReceived(self, data):
self.resetTimeout()
if self.status == 'READY': if self.status == 'READY':
packet_type = ord(data[0]) packet_type = ord(data[0])
if packet_type == 0: # Keepalive if packet_type == 0: # Keepalive
self.ready()
self.pong() self.pong()
elif packet_type == 1: # TX ack elif packet_type == 1: # TX ack
self.status = 'TX' self.status = 'TX'
self.phase = 0 self.expectRawData(2)
if len(data) > 1:
self.dataReceived(data[1:])
elif packet_type == 2: # Audio elif packet_type == 2: # Audio
self.startAudioMessage(data[1:]) self.status = 'AUDIO'
self.expectRawData(327) # Two ID bytes + 10 GSM frames
elif packet_type == 3: # Client list elif packet_type == 3: # Client list
self.startMultiLineMessage('CLIENTS', data[1:]) self.status = 'CLIENTS'
self.expectRawData(2) # Discard two null bytes
elif packet_type == 4: # Text elif packet_type == 4: # Text
self.startMultiLineMessage('TEXT', data[1:]) self.startMultiLineMessage('TEXT')
elif packet_type == 5: # Channel list elif packet_type == 5: # Channel list
self.startMultiLineMessage('NETWORKS', data[1:]) self.startMultiLineMessage('NETWORKS')
elif packet_type == 6: # Admin list
self.startMultiLineMessage('ADMIN')
elif packet_type == 7: # Access list
self.startMultiLineMessage('ACCESS')
elif packet_type == 8: # Block list
self.startMultiLineMessage('BLOCK')
elif packet_type == 9: # Mute list
self.startMultiLineMessage('MUTE')
elif packet_type == 10: # Access list flags
self.startMultiLineMessage('ACCESSFLAGS')
else: else:
log.err("Unknown packet type %d" % packet_type) log.err("Unknown packet type %d" % packet_type)
elif self.status == 'CLIENTS':
self.startMultiLineMessage('CLIENTS')
elif self.status == 'AUDIO': elif self.status == 'AUDIO':
self.collectAudioMessage(data) self.ready()
self.decodeAUDIO(ord(data[0])*256+ord(data[1]), data[2:])
elif self.status == 'TX': elif self.status == 'TX':
if self.phase == 0: self.ready()
self.phase = 1 self.decodeTX(ord(data[0])*256+ord(data[1]))
self.decodeTX(ord(data[0])*256+ord(data[1]))
self.ready()
if len(data) > 2:
self.dataReceived(data[2:])
def login(self): def login(self):
d = self.factory.client_id ap = "CT:"+self.user.asXML(
fields = [ 'VX','EA','PW','ON','BC','DS','NN','CT','NT')
('VX', self.client_version),
('EA', d['email']),
('PW', d['password']),
('ON', d['operator']),
('BC', d['transmission']),
('DS', d['description']),
('NN', d['country']),
('CT', d['city']),
('NT', d['network'])
]
ap = "CT:"+formatSimpleXML(fields)
self.status = 'AUTH' self.status = 'AUTH'
self.phase = 0 self.phase = 0
self.sendLine(ap) self.sendLine(ap)
@ -137,6 +133,7 @@ class FRNClient(LineReceiver):
self.sendLine('TX0') self.sendLine('TX0')
def sendAudioFrame(self, frame): def sendAudioFrame(self, frame):
self.resetTimeout()
self.sendLine('TX1') self.sendLine('TX1')
self.transport.write(frame) self.transport.write(frame)
@ -168,8 +165,52 @@ class FRNClient(LineReceiver):
def sendTextMessage(self, dest, text): def sendTextMessage(self, dest, text):
self.sendLine('TM:'+formatSimpleXML(dict(ID=dest, MS=text))) self.sendLine('TM:'+formatSimpleXML(dict(ID=dest, MS=text)))
def unimplemented(self, msg): def addAdmin(self, client_ip):
log.msg("Unimplemented: %s" % msg) self.sendLine("AA:"+formatSimpleXML(dict(IP=client_ip)))
def removeAdmin(self, client_ip):
self.sendLine("DA:"+formatSimpleXML(dict(IP=client_ip)))
def addMute(self, client_ip):
self.sendLine("MC:"+formatSimpleXML(dict(IP=client_ip)))
def removeMute(self, client_ip):
self.sendLine("UM:"+formatSimpleXML(dict(IP=client_ip)))
def addBlock(self, client_ip):
self.sendLine("BC:"+formatSimpleXML(dict(IP=client_ip)))
def removeBlock(self, client_ip):
self.sendLine("UC:"+formatSimpleXML(dict(IP=client_ip)))
def addAccess(self, email):
self.sendLine("AT:"+formatSimpleXML(dict(EA=email)))
def removeAccess(self, email):
self.sendLine("DT:"+formatSimpleXML(dict(EA=email)))
def addTalk(self, email):
self.sendLine("ETX:"+formatSimpleXML(dict(EA=email)))
def removeTalk(self, email):
self.sendLine("RTX:"+formatSimpleXML(dict(EA=email)))
def accessFlagEnable(self, enable):
if enable:
v = 1
else:
v = 0
self.sendLine("ENA:%d" % v)
def accessFlagTalk(self, enable):
if enable:
v = 1
else:
v = 0
self.sendLine("TXR:%d" % v)
def unimplemented(self, status, msg):
log.msg("Unimplemented: %s: %s" % (status, msg))
def decodeAUDIO(self, from_id, frames): def decodeAUDIO(self, from_id, frames):
self.audioFrameReceived(from_id, frames) self.audioFrameReceived(from_id, frames)
@ -186,6 +227,24 @@ class FRNClient(LineReceiver):
def decodeNETWORKS(self, msg): def decodeNETWORKS(self, msg):
self.networksListUpdated(msg) self.networksListUpdated(msg)
def decodeADMIN(self, msg):
self.adminListUpdated([parseSimpleXML(x) for x in msg])
def decodeACCESS(self, msg):
self.accessListUpdated([parseSimpleXML(x) for x in msg])
def decodeBLOCK(self, msg):
self.blockListUpdated([parseSimpleXML(x) for x in msg])
def decodeMUTE(self, msg):
self.muteListUpdated([parseSimpleXML(x) for x in msg])
def decodeACCESSFLAGS(self, msg):
self.accessFlagsUpdated(msg[0], msg[1])
def decodeUNKNOWN(self, code, msg):
log.msg("%s: %s" % (code, msg))
def loginResponse(self, info): def loginResponse(self, info):
pass pass
@ -201,25 +260,47 @@ class FRNClient(LineReceiver):
def networksListUpdated(self, networks): def networksListUpdated(self, networks):
pass pass
def adminListUpdated(self, admins):
pass
class FRNClientFactory(ClientFactory): def accessListUpdated(self, access):
pass
def blockListUpdated(self, blocks):
pass
def muteListUpdated(self, mutes):
pass
def accessFlagsUpdated(self, access, talk):
pass
class FRNClientFactory(ReconnectingClientFactory):
protocol = FRNClient protocol = FRNClient
maxRetries = 10
def __init__(self, **kw): def __init__(self, user):
self.client_id = kw self.user = user
def startedConnecting(self, connector): def startedConnecting(self, connector):
log.msg('Started to connect') log.msg('Started to connect')
def buildProtocol(self, addr): def buildProtocol(self, addr):
log.msg('Connected') log.msg('Connected')
return ClientFactory.buildProtocol(self, addr) p = ReconnectingClientFactory.buildProtocol(self, addr)
p.user = self.user
return p
def clientConnectionLost(self, connector, reason): def clientConnectionLost(self, connector, reason):
log.msg('Lost connection. Reason: %s' % reason) log.msg('Lost connection. Reason: %s' % reason)
ReconnectingClientFactory.clientConnectionLost(
self, connector, reason)
def clientConnectionFailed(self, connector, reason): def clientConnectionFailed(self, connector, reason):
log.err('Connection failed. Reason: %s' % reason) log.err('Connection failed. Reason: %s' % reason)
ReconnectingClientFactory.clientConnectionFailed(
self, connector, reason)
# vim: set et ai sw=4 ts=4 sts=4: # vim: set et ai sw=4 ts=4 sts=4:

View File

@ -1,9 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
#
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
# See LICENSE.txt for copyright info
from HTMLParser import HTMLParser from HTMLParser import HTMLParser
from random import choice
def makeAuthKey(kp): def responseToChallange(kp):
if len(kp) != 6: if len(kp) != 6:
return 'ERROR' return 'ERROR'
aa, bb, cc = int(kp[:2]), int(kp[2:4]), int(kp[4:6]) aa, bb, cc = int(kp[:2]), int(kp[2:4]), int(kp[4:6])
@ -11,6 +15,10 @@ def makeAuthKey(kp):
return defgh[3]+defgh[0]+defgh[2]+defgh[4]+defgh[1] return defgh[3]+defgh[0]+defgh[2]+defgh[4]+defgh[1]
def makeRandomChallange():
return ''.join([choice('0123456789') for i in range(6)])
class SimpleXMLParser(HTMLParser): class SimpleXMLParser(HTMLParser):
def handle_starttag(self, tag, attrs): def handle_starttag(self, tag, attrs):

View File

@ -1,8 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
#
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
# See LICENSE.txt for copyright info
from __future__ import with_statement from __future__ import with_statement
from frn.protocol.client import FRNClient, FRNClientFactory from frn.protocol.client import FRNClient, FRNClientFactory
from frn.user import FRNUser
from twisted.internet import reactor, task from twisted.internet import reactor, task
from twisted.internet.defer import DeferredList from twisted.internet.defer import DeferredList
from twisted.python import log from twisted.python import log
@ -92,25 +96,37 @@ class FRNParrotFactory(FRNClientFactory):
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
from os.path import dirname, join as pjoin
from ConfigParser import ConfigParser from ConfigParser import ConfigParser
log.startLogging(sys.stderr) log.startLogging(sys.stderr)
cfg = ConfigParser() basedir = dirname(__file__)
cfg.read('stations.conf')
if len(sys.argv) > 1: acfg = ConfigParser()
station = sys.argv[1] acfg.read(['/etc/grn/accounts.conf',
else: pjoin(basedir,'accounts.conf'), 'accounts.conf'])
stations = cfg.sections()
stations.sort()
station = cfg.sections()[0]
log.msg("Profile not specified: using '%s'" % station)
station_conf = dict(cfg.items(station)) scfg = ConfigParser()
scfg.read(['/etc/grn/servers.conf',
pjoin(basedir,'servers.conf'), 'servers.conf'])
argc = len(sys.argv)
if argc >= 3:
server_name, network_name = sys.argv[2].split(':',1)
account_cfg = acfg.items(sys.argv[1])+[('network', network_name)]
server_cfg = scfg.items(server_name)
server = scfg.get(server_name, 'server')
port = scfg.getint(server_name, 'port')
d = dict(account_cfg)
user = FRNUser(
EA=d['email'],
PW=d['password'], ON=d['operator'],
BC=d['transmission'], DS=d['description'],
NN=d['country'], CT=d['city'], NT=d['network'])
reactor.connectTCP(server, port, FRNParrotFactory(user))
reactor.run()
reactor.connectTCP('master.freeradionetwork.it', 10024,
FRNParrotFactory(**station_conf))
reactor.run()
# vim: set et ai sw=4 ts=4 sts=4: # vim: set et ai sw=4 ts=4 sts=4: