diff --git a/sso/services/ts3/__init__.py b/sso/services/ts3/__init__.py new file mode 100644 index 0000000..e04b386 --- /dev/null +++ b/sso/services/ts3/__init__.py @@ -0,0 +1,114 @@ +import urllib +from sso.services import BaseService +from ts3 import TS3Server + +class TS3Service(BaseService): + + settings = { 'require_user': True, + 'require_password': False, + 'provide_login': False, + 'host': 'mumble.pleaseignore.com', + 'port': 10011, + 'sa_login': 'serveradmin', + 'sa_password': '', + 'vhost_id': 0, + 'authed_sgid': 12, + 'name_format': '%(alliance)s - %(corporation)s - %(name)s'} + + def __init__(self): + pass + + @property + def _conn(self): + if not hasattr(self, '__conn'): + self.__conn = TS3Server(self.settings['host'], self.settings['port']) + self.__conn.login(self.settings['sa_login'], self.settings['sa_password']) + self.__conn.use(self.settings['vhost_id']) + + return self.__conn + + def add_user(self, username, password, **kwargs): + """ Add a user, returns a UID for that user """ + + details = { 'name': username, + 'alliance': kwargs['character'].corporation.alliance.ticker, + 'corporation': kwargs['character'].corporation.ticker } + + username = self.settings['name_format'] % details + ret = self._conn.send_command('tokenadd', {'tokentype': 0, 'tokenid1': self.settings['authed_sgid'], 'tokenid2': 0, 'tokendescription': "Auth Token for %s" % username, 'tokencustomset': "ident=sso_uid value=%s" % username }) + + if 'keys' in ret and 'token' in ret['keys']: + token = ret['keys']['token'] + url = "Register" % (self.settings['host'], urllib.urlencode({'nickname': username, 'addbookmark': 'TEST Alliance TS3', 'token': token})) + return { 'username': username, 'permission token': token, 'registration url': url } + + return None + + def check_user(self, uid): + """ Check if the username exists """ + # Lookup user using customsearch with sso_uid=uid + return self._get_userid(uid) + + def delete_user(self, uid): + """ Delete a user by uid """ + user = self._get_userid(uid) + if user: + ret = self._conn.send_command('clientdbdelete', {'cldbid': user }) + return True + + def disable_user(self, uid): + """ Disable a user by uid """ + user = self._get_userid(uid) + if user: + ret = self._conn.send_command('servergroupdelclient', {'sgid': self.settings['authed_sgid'], 'cldbid': user }) + return True + + def enable_user(self, uid, password): + """ Enable a user by uid """ + + user = self._get_userid(uid) + if user: + ret = self._conn.send_command('servergroupaddclient', {'sgid': self.settings['authed_sgid'], 'cldbid': user }) + return True + + def reset_password(self, uid, password): + """ Reset the user's password """ + pass + + def login(uid): + """ Login the user and provide cookies back """ + pass + + def _get_userid(self, uid): + """ Finds the TS3 ID of a user from their UID """ + + ret = self._conn.send_command('customsearch', {'ident': 'sso_uid', 'pattern': 'uid'}) + if ret and type(ret) == type(list): + return ret[0]['cldbid'] + + def _create_groups(self, groups): + agroups = groups.values_list('name') + ngroups = agroups.copy() + for tsg in self._conn.send_command('servergrouplist'): + ngroups.remove(tsg['keys']['name']) + + if len(ngroups): + for g in ngroups: + self._conn.send_command('servergroupadd', { 'name': g }) + + ret = [] + for tsg in self._conn.send_command('servergrouplist'): + if tsg['keys']['name'] in agroups: + ret.append(tsg['keys']) + return ret + + def update_groups(self, uid, groups): + """ Update the UID's groups based on the provided list """ + + if groups.count(): + addgroups = self._create_groups(groups) + user = self._get_userid(uid) + for g in addgroups: + ret = self._conn.send_command('servergroupaddclient', {'sgid': g['sgid'], 'cldbid': user }) + +ServiceClass = 'TS3Service' diff --git a/sso/services/ts3/ts3.py b/sso/services/ts3/ts3.py new file mode 100644 index 0000000..f614b41 --- /dev/null +++ b/sso/services/ts3/ts3.py @@ -0,0 +1,252 @@ +import socket +import logging + +class ConnectionError(): + + def __init__(self, ip, port): + self.ip = ip + self.port = port + + def __str__(): + return 'Error connecting to host %s port %s' % (self.ip, self.port) + +ts3_escape = { '/': r"\/", + ' ': r'\s', + '|': r'\p', + "\a": r'\a', + "\b": r'\b', + "\f": r'\f', + "\n": r'\n', + "\r": r'\r', + "\t": r'\t', + "\v": r'\v' } + + +class TS3Proto(): + + bytesin = 0 + bytesout = 0 + + _connected = False + + def __init__(self): + self._log = logging.getLogger('%s.%s' % (__name__, self.__class__.__name__)) + pass + + def connect(self, ip, port): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.connect((ip, port)) + except: + #raise ConnectionError(ip, port) + raise + else: + self._sock = s + self._sockfile = s.makefile('r', 0) + + data = self._sockfile.readline() + if data.strip() == "TS3": + self._connected = True + return True + + def disconnect(self): + self.send_command("quit") + self._sock.close() + self._sock = None + self._connected = False + self._log.info('Disconnected') + + def send_command(self, command, keys=None, opts=None): + cmd = self.construct_command(command, keys=keys, opts=opts) + self.send('%s\n' % cmd) + + data = [] + + while True: + resp = self._sockfile.readline() + resp = self.parse_command(resp) + if not 'command' in resp: + data.append(resp) + else: + break + + if resp['command'] == 'error': + if data and resp['keys']['id'] == '0': + if len(data) > 1: + return data + else: + return data[0] + else: + return resp['keys']['id'] + + def construct_command(self, command, keys=None, opts=None): + """ + Constructs a TS3 formatted command string + + Keys can have a single nested list to construct a nested parameter + + @param command: Command list + @type command: string + @param keys: Key/Value pairs + @type keys: dict + @param opts: Options + @type opts: list + """ + + cstr = [command] + + # Add the keys and values, escape as needed + if keys: + for key in keys: + if isinstance(keys[key], list): + ncstr = [] + for nest in keys[key]: + ncstr.append("%s=%s" % (key, self._escape_str(nest))) + cstr.append("|".join(ncstr)) + else: + cstr.append("%s=%s" % (key, self._escape_str(keys[key]))) + + # Add in options + if opts: + for opt in opts: + cstr.append("-%s" % opt) + + return " ".join(cstr) + + def parse_command(self, commandstr): + """ + Parses a TS3 command string into command/keys/opts tuple + + @param commandstr: Command string + @type commandstr: string + """ + + if len(commandstr.split('|')) > 1: + vals = [] + for cmd in commandstr.split('|'): + vals.append(self.parse_command(cmd)) + return vals + + cmdlist = commandstr.strip().split(' ') + command = None + keys = {} + opts = [] + + for key in cmdlist: + v = key.strip().split('=') + if len(v) > 1: + # Key + key, value = v + keys[key] = self._unescape_str(value) + elif v[0][0] == '-': + # Option + opts.append(v[0][1:]) + else: + command = v[0] + + d = {'keys': keys, 'opts': opts} + if command: + d['command'] = command + return d + + @staticmethod + def _escape_str(value): + """ + Escape a value into a TS3 compatible string + + @param value: Value + @type value: string/int + + """ + + if isinstance(value, int): return "%d" % value + value = value.replace("\\", r'\\') + for i, j in ts3_escape.iteritems(): + value = value.replace(i, j) + return value + + @staticmethod + def _unescape_str(value): + """ + Unescape a TS3 compatible string into a normal string + + @param value: Value + @type value: string/int + + """ + + if isinstance(value, int): return "%d" % value + value = value.replace(r"\\", "\\") + for i, j in ts3_escape.iteritems(): + value = value.replace(j, i) + return value + + + def send(self, payload): + if self._connected: + self._log.debug('Sent: %s' % payload) + self._sockfile.write(payload) + + +class TS3Server(TS3Proto): + def __init__(self, ip, port, id=0, sock=None): + """ + Abstraction class for TS3 Servers + + @param ip: IP Address + @type ip: str + @param port: Port Number + @type port: int + + """ + TS3Proto.__init__(self) + + if not sock: + if self.connect(ip, port) and id > 0: + self.use(id) + else: + self._sock = sock + self._sockfile = sock.makefile('r', 0) + self._connected = True + + def login(self, username, password): + """ + Login to the TS3 Server + + @param username: Username + @type username: str + @param password: Password + @type password: str + """ + d = self.send_command('login', keys={'client_login_name': username, 'client_login_password': password }) + if d == 0: + self._log.info('Login Successful') + return True + return False + + def serverlist(self): + """ + Get a list of all Virtual Servers on the connected TS3 instance + """ + if self._connected: + return self.send_command('serverlist') + + def gm(self, msg): + """ + Send a global message to the current Virtual Server + + @param msg: Message + @type ip: str + """ + if self._connected: + return self.send_command('gm', keys={'msg': msg}) + + def use(self, id): + """ + Use a particular Virtual Server instance + + @param id: Virtual Server ID + @type id: int + """ + if self._connected and id > 0: + self.send_command('use', keys={'sid': id})