#!/usr/bin/env python3
import sys, socket, threading, re, time, logging, uuid, xml.etree.ElementTree as ET
class XMPPServer():
server_id = 'bumper'
bot_id = 'bumpy'
client_id = None
clients = []
exit_flag = False
def __init__(self, address):
try:
# Initialize bot server
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(address)
self.socket.listen(1)
logging.info('XMPPServer: listening on {}:{}'.format(address[0], address[1]))
while not self.exit_flag:
logging.info('XMPPServer: awaiting connection')
connection, client_address = self.socket.accept()
# disconnect any clients with this ip
for client in self.clients:
if client.address == client_address[0]:
client.disconnect()
thread_id = uuid.uuid4()
client = Client(thread_id, connection, client_address)
client.start()
self.clients.append(client)
self.socket.close()
except Exception as e:
logging.error('XMPPServer: {}'.format(e))
except KeyboardInterrupt:
logging.debug('XMPPServer: Keyboard interrupt')
finally:
self.disconnect()
logging.info('XMPPServer: bye')
def disconnect(self):
logging.info('XMPPServer: waiting for all client threads to exit')
for client in self.clients:
client.disconnect()
client.join()
self.exit_flag = True
logging.info('XMPPServer: shutting down...')
class Client(threading.Thread):
IDLE = 0
CONNECT = 1
INIT = 2
BIND = 3
READY = 4
DISCONNECT = 5
UNKNOWN = 0
BOT = 1
CONTROLLER = 2
def __init__(self, thread_id, connection, client_address):
threading.Thread.__init__(self)
self.id = thread_id
self.name = "XMPP Thread {}".format(self.id)
self.type = self.UNKNOWN
self.state = self.IDLE
self.connection = connection
self.address = client_address[0]
def send(self, command):
logging.debug('to {}: {}'.format(self.address, command))
self.connection.send(command.encode())
def disconnect(self):
logging.info('{} disconnecting'.format(self.address))
self.connection.close()
self._set_state('DISCONNECT')
def _tag_strip_uri(self, tag):
if tag[0] == '{':
uri, ignore, tag = tag[1:].partition('}')
return tag
def _set_state(self, state):
new_state = getattr(Client, state)
if self.state > new_state:
raise Exception('{} illegal state change {}->{}'.format(self.address, self.state, new_state))
logging.info('{} state: {}'.format(self.address, state))
self.state = new_state
def _handle_ctl(self, xml, data):
ctl = xml[0][0]
if ctl.get('admin') and self.type == self.BOT:
logging.info('admin username received from bot: {}'.format(ctl.get('admin')))
XMPPServer.client_id = ctl.get('admin')
return
# forward
for client in XMPPServer.clients:
if client.address != self.address and client.state == client.READY:
if client.type == self.BOT:
data = data.decode('utf-8')
id_index = data.find('id')
if id_index > -1:
data = data[:id_index] + 'from="' + XMPPServer.client_id + '" ' + data[id_index:]
data = data.encode()
client.send(data.decode('utf-8'))
def _handle_ping(self, xml, data):
if(xml.get('to').find('@') == -1):
# Ping to server - respond
self.send(''.format(xml.get('id'), xml.get('to')))
else:
for client in XMPPServer.clients:
if client.address != self.address and client.state == client.READY:
client.send(data.decode('utf-8'))
def _handle_result(self, data):
# forward
for client in XMPPServer.clients:
if client.address != self.address and client.state == client.READY:
client.send(data.decode('utf-8'))
def run(self):
try:
logging.info('client connected: {}'.format(self.address))
self._set_state('CONNECT')
while True:
if not self.connection._closed:
data = self.connection.recv(4096)
if data:
logging.debug('from {}: {}'.format(self.address, data.decode('utf-8')))
try:
if self.state == self.CONNECT:
if data.decode('utf-8').find('jabber:client') > -1:
self._set_state('INIT')
# ack jabbr:client
self.send(''.format(XMPPServer.server_id))
time.sleep(0.5)
# session
self.send('')
continue
xml = ET.fromstring(data)
if len(xml):
child = self._tag_strip_uri(xml[0].tag)
else:
child = None
if xml.tag == 'iq':
res = None
if child == 'bind':
res = '{}'.format(xml.get('id'), XMPPServer.bot_id)
self._set_state('BIND')
elif child == 'session':
res = ''.format(xml.get('id'))
self._set_state('READY')
elif child == 'query':
self._handle_ctl(xml, data)
elif child == 'ping':
self._handle_ping(xml, data)
elif xml.get('type') == 'result':
self._handle_result(data)
if res:
self.send(res)
elif xml.tag == 'presence':
if len(xml) and xml[0].tag == 'status':
# bot announcing arrival
self.type = self.BOT
logging.info('{} type set to BOT (based on presence tag)'.format(self.address))
# send a command from an unknown user - the response will contain the correct admin username
self.send(''.format(uuid.uuid4(), 'unknown@ecouser.net', XMPPServer.bot_id))
elif xml.get('type') == 'available':
self.type = self.CONTROLLER
logging.info('{} type set to CONTROLLER (based on presence tag)'.format(self.address))
except ET.ParseError as e:
logging.debug('error: {}'.format(e))
except Exception as e:
logging.error('XMPPServer: {}'.format(e))
self._set_state('DISCONNECT')
except OSError as e:
logging.error('XMPPServer: {}'.format(e))
self._set_state('DISCONNECT')
except Exception as e:
logging.error('XMPPServer: {}'.format(e))
self._set_state('DISCONNECT')
finally:
self.disconnect()