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:
parent
abd4301390
commit
1a19977790
|
@ -1,5 +1,7 @@
|
|||
*.py[co]
|
||||
*~
|
||||
stations.conf
|
||||
accounts.conf
|
||||
servers.conf
|
||||
recordings/*
|
||||
sounds/*
|
||||
*.sqlite3
|
||||
|
|
|
@ -1,66 +1,59 @@
|
|||
# -*- 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
|
||||
else:
|
||||
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:])
|
||||
if self.phase >= self.expected_lines:
|
||||
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)
|
||||
self.ready()
|
||||
self.decodeAUDIO(ord(data[0])*256+ord(data[1]), data[2:])
|
||||
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.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)
|
||||
log.msg('Connected')
|
||||
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:
|
||||
|
|
|
@ -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):
|
||||
|
|
42
parrot.py
42
parrot.py
|
@ -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'])
|
||||
|
||||
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:
|
||||
|
|
Loading…
Reference in New Issue