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]
*~
stations.conf
accounts.conf
servers.conf
recordings/*
sounds/*
*.sqlite3

View File

@ -1,36 +1,51 @@
# -*- coding: utf-8 -*-
#
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
# See LICENSE.txt for copyright info
from Queue import Queue
from twisted.internet.protocol import ClientFactory
from twisted.protocols.basic import LineReceiver
from twisted.internet.protocol import ReconnectingClientFactory
from twisted.protocols.policies import TimeoutMixin
from twisted.internet.task import LoopingCall
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 *
class FRNClient(LineReceiver):
client_version = 2010002
class FRNClient(BufferingLineReceiver, TimeoutMixin):
def connectionMade(self):
BufferingLineReceiver.connectionMade(self)
self.user.VX = versions.client
self.txq = Queue()
self.setTimeout(25.0)
self.login()
def ready(self):
self.status = 'READY'
self.msgbuffer = []
self.phase = 0
self.setRawMode()
self.expectRawData(1)
def startMultiLineMessage(self, msgtype, rest=''):
def startMultiLineMessage(self, msgtype):
self.status = msgtype
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):
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.msgbuffer = []
self.phase = 0
@ -38,29 +53,7 @@ class FRNClient(LineReceiver):
self.msgbuffer.append(line)
self.phase += 1
if self.phase >= self.expected_lines:
handler = getattr(self, 'decode'+self.status, self.unimplemented)
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:])
self.stopMultiLineMessage()
def lineReceived(self, line):
if self.status == 'AUTH':
@ -69,57 +62,60 @@ class FRNClient(LineReceiver):
self.phase = 1
else:
self.serverdata = parseSimpleXML(line.strip())
self.loginResponse(self.serverdata)
if int(self.serverdata['sv']) > 2009004:
self.sendLine(makeAuthKey(self.serverdata['kp']))
self.sendLine(responseToChallange(
self.serverdata['kp']))
self.ready()
self.setTimeout(10.0)
self.factory.resetDelay()
self.loginResponse(self.serverdata)
else:
self.collectMultiLineMessage(line)
def rawDataReceived(self, data):
def expectedReceived(self, data):
self.resetTimeout()
if self.status == 'READY':
packet_type = ord(data[0])
if packet_type == 0: # Keepalive
self.ready()
self.pong()
elif packet_type == 1: # TX ack
self.status = 'TX'
self.phase = 0
if len(data) > 1:
self.dataReceived(data[1:])
self.expectRawData(2)
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
self.startMultiLineMessage('CLIENTS', data[1:])
self.status = 'CLIENTS'
self.expectRawData(2) # Discard two null bytes
elif packet_type == 4: # Text
self.startMultiLineMessage('TEXT', data[1:])
self.startMultiLineMessage('TEXT')
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:
log.err("Unknown packet type %d" % packet_type)
elif self.status == 'CLIENTS':
self.startMultiLineMessage('CLIENTS')
elif self.status == 'AUDIO':
self.collectAudioMessage(data)
elif self.status == 'TX':
if self.phase == 0:
self.phase = 1
self.decodeTX(ord(data[0])*256+ord(data[1]))
self.ready()
if len(data) > 2:
self.dataReceived(data[2:])
self.decodeAUDIO(ord(data[0])*256+ord(data[1]), data[2:])
elif self.status == 'TX':
self.ready()
self.decodeTX(ord(data[0])*256+ord(data[1]))
def login(self):
d = self.factory.client_id
fields = [
('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)
ap = "CT:"+self.user.asXML(
'VX','EA','PW','ON','BC','DS','NN','CT','NT')
self.status = 'AUTH'
self.phase = 0
self.sendLine(ap)
@ -137,6 +133,7 @@ class FRNClient(LineReceiver):
self.sendLine('TX0')
def sendAudioFrame(self, frame):
self.resetTimeout()
self.sendLine('TX1')
self.transport.write(frame)
@ -168,8 +165,52 @@ class FRNClient(LineReceiver):
def sendTextMessage(self, dest, text):
self.sendLine('TM:'+formatSimpleXML(dict(ID=dest, MS=text)))
def unimplemented(self, msg):
log.msg("Unimplemented: %s" % msg)
def addAdmin(self, client_ip):
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):
self.audioFrameReceived(from_id, frames)
@ -186,6 +227,24 @@ class FRNClient(LineReceiver):
def decodeNETWORKS(self, 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):
pass
@ -201,25 +260,47 @@ class FRNClient(LineReceiver):
def networksListUpdated(self, networks):
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
maxRetries = 10
def __init__(self, **kw):
self.client_id = kw
def __init__(self, user):
self.user = user
def startedConnecting(self, connector):
log.msg('Started to connect')
def buildProtocol(self, addr):
log.msg('Connected')
return ClientFactory.buildProtocol(self, addr)
p = ReconnectingClientFactory.buildProtocol(self, addr)
p.user = self.user
return p
def clientConnectionLost(self, connector, reason):
log.msg('Lost connection. Reason: %s' % reason)
ReconnectingClientFactory.clientConnectionLost(
self, connector, reason)
def clientConnectionFailed(self, connector, reason):
log.err('Connection failed. Reason: %s' % reason)
ReconnectingClientFactory.clientConnectionFailed(
self, connector, reason)
# vim: set et ai sw=4 ts=4 sts=4:

View File

@ -1,9 +1,13 @@
# -*- coding: utf-8 -*-
#
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
# See LICENSE.txt for copyright info
from HTMLParser import HTMLParser
from random import choice
def makeAuthKey(kp):
def responseToChallange(kp):
if len(kp) != 6:
return 'ERROR'
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]
def makeRandomChallange():
return ''.join([choice('0123456789') for i in range(6)])
class SimpleXMLParser(HTMLParser):
def handle_starttag(self, tag, attrs):

View File

@ -1,8 +1,12 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2010 Maurizio Porrato <maurizio.porrato@gmail.com>
# See LICENSE.txt for copyright info
from __future__ import with_statement
from frn.protocol.client import FRNClient, FRNClientFactory
from frn.user import FRNUser
from twisted.internet import reactor, task
from twisted.internet.defer import DeferredList
from twisted.python import log
@ -92,25 +96,37 @@ class FRNParrotFactory(FRNClientFactory):
if __name__ == '__main__':
import sys
from os.path import dirname, join as pjoin
from ConfigParser import ConfigParser
log.startLogging(sys.stderr)
cfg = ConfigParser()
cfg.read('stations.conf')
basedir = dirname(__file__)
if len(sys.argv) > 1:
station = sys.argv[1]
else:
stations = cfg.sections()
stations.sort()
station = cfg.sections()[0]
log.msg("Profile not specified: using '%s'" % station)
acfg = ConfigParser()
acfg.read(['/etc/grn/accounts.conf',
pjoin(basedir,'accounts.conf'), 'accounts.conf'])
station_conf = dict(cfg.items(station))
scfg = ConfigParser()
scfg.read(['/etc/grn/servers.conf',
pjoin(basedir,'servers.conf'), 'servers.conf'])
reactor.connectTCP('master.freeradionetwork.it', 10024,
FRNParrotFactory(**station_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()
# vim: set et ai sw=4 ts=4 sts=4: