diff --git a/.gitignore b/.gitignore index 8e0cd3c..34540df 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *.py[co] *~ -stations.conf +accounts.conf +servers.conf recordings/* sounds/* +*.sqlite3 diff --git a/frn/protocol/client.py b/frn/protocol/client.py index 68bf730..d269d5f 100644 --- a/frn/protocol/client.py +++ b/frn/protocol/client.py @@ -1,66 +1,59 @@ # -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# 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 + + def accessListUpdated(self, access): + pass + + def blockListUpdated(self, blocks): + pass + + def muteListUpdated(self, mutes): + pass + + def accessFlagsUpdated(self, access, talk): + pass + -class FRNClientFactory(ClientFactory): +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: diff --git a/frn/utility.py b/frn/utility.py index a2d233c..9738161 100644 --- a/frn/utility.py +++ b/frn/utility.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# 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): diff --git a/parrot.py b/parrot.py index 6a3c57e..98f6bfa 100755 --- a/parrot.py +++ b/parrot.py @@ -1,8 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +# +# Copyright 2010 Maurizio Porrato +# 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__) + + 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: + 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() - 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) - - station_conf = dict(cfg.items(station)) - - reactor.connectTCP('master.freeradionetwork.it', 10024, - FRNParrotFactory(**station_conf)) - reactor.run() # vim: set et ai sw=4 ts=4 sts=4: