from direct.directnotify.DirectNotifyGlobal import directNotify from direct.distributed.DistributedObjectGlobalUD import DistributedObjectGlobalUD from direct.distributed.PyDatagram import * from direct.fsm.FSM import FSM from otp.ai.MagicWordGlobal import * from otp.distributed import OtpDoGlobals from toontown.makeatoon.NameGenerator import NameGenerator from toontown.toon.ToonDNA import ToonDNA from toontown.toonbase import TTLocalizer from toontown.uberdog import NameJudgeBlacklist from panda3d.core import * import hashlib, hmac, json import anydbm, math, os import urllib2, time, urllib import cookielib, socket def rejectConfig(issue, securityIssue=True, retarded=True): print print print 'Lemme get this straight....' print 'You are trying to use remote account database type...' print 'However,', issue + '!!!!' if securityIssue: print 'Do you want this server to get hacked?' if retarded: print '"Either down\'s or autism"\n - JohnnyDaPirate, 2015' print 'Go fix that!' exit() def entropy(string): prob = [float(string.count(c)) / len(string) for c in dict.fromkeys(list(string))] entropy = -sum([p * math.log(p) / math.log(2.0) for p in prob]) return entropy def entropyIdeal(length): prob = 1.0 / length return -length * prob * math.log(prob) / math.log(2.0) accountDBType = config.GetString('accountdb-type', 'developer') accountServerSecret = config.GetString('account-server-secret', 'dev') accountServerHashAlgo = config.GetString('account-server-hash-algo', 'sha512') apiSecret = accountServerSecret = config.GetString('api-key', 'dev') if accountDBType == 'remote': if accountServerSecret == 'dev': rejectConfig('you have not changed the secret in config/local.prc') if apiSecret == 'dev': rejectConfig('you have not changed the api key in config/local.prc') if len(accountServerSecret) < 16: rejectConfig('the secret is too small! Make it 16+ bytes', retarded=False) secretLength = len(accountServerSecret) ideal = entropyIdeal(secretLength) / 2 entropy = entropy(accountServerSecret) if entropy < ideal: rejectConfig('the secret entropy is too low! For %d bytes,' ' it should be %d. Currently it is %d' % (secretLength, ideal, entropy), retarded=False) hashAlgo = getattr(hashlib, accountServerHashAlgo, None) if not hashAlgo: rejectConfig('%s is not a valid hash algo' % accountServerHashAlgo, securityIssue=False) hashSize = len(hashAlgo('').digest()) minAccessLevel = config.GetInt('min-access-level', 100) def executeHttpRequest(url, **extras): # TO DO: THIS IS QUITE DISGUSTING # MOVE THIS TO ToontownInternalRepository (this might be interesting for AI) ##### USE PYTHON 2.7.9 ON PROD WITH SSL AND CLOUDFLARE ##### _data = {} if len(extras.items()) != 0: for k, v in extras.items(): _data[k] = v signature = hashlib.sha512(json.dumps(_data) + apiSecret).hexdigest() data = urllib.urlencode({'data': json.dumps(_data), 'hmac': signature}) cookie_jar = cookielib.LWPCookieJar() cookie = urllib2.HTTPCookieProcessor(cookie_jar) opener = urllib2.build_opener(cookie) req = urllib2.Request('http://192.168.1.212/api/' + url, data, headers={"Content-Type" : "application/x-www-form-urlencoded"}) req.get_method = lambda: "POST" try: return opener.open(req).read() except: return None notify = directNotify.newCategory('ClientServicesManagerUD') def executeHttpRequestAndLog(url, **extras): # SEE ABOVE response = executeHttpRequest(url, extras) if response is None: notify.error('A request to ' + url + ' went wrong.') return None try: data = json.loads(response) except: notify.error('Malformed response from ' + url + '.') return None if 'error' in data: notify.warning('Error from ' + url + ':' + data['error']) return data #blacklist = executeHttpRequest('names/blacklist.json') # todo; create a better system for this blacklist = json.dumps(["none"]) if blacklist: blacklist = json.loads(blacklist) def judgeName(name): if not name: return False if blacklist: for namePart in name.split(' '): namePart = namePart.lower() if len(namePart) < 1: return False for banned in blacklist: if banned in namePart: return False return True # --- ACCOUNT DATABASES --- # These classes make up the available account databases for Toontown Stride. # DeveloperAccountDB is a special database that accepts a username, and assigns # each user with 700 access automatically upon login. class AccountDB: notify = directNotify.newCategory('AccountDB') def __init__(self, csm): self.csm = csm filename = config.GetString('account-bridge-filename', 'account-bridge.db') filename = os.path.join('dependencies', filename) self.dbm = anydbm.open(filename, 'c') def addNameRequest(self, avId, name, accountID = None): return True def getNameStatus(self, accountId, callback = None): return 'APPROVED' def removeNameRequest(self, avId): pass def lookup(self, data, callback): userId = data['userId'] data['success'] = True data['accessLevel'] = max(data['accessLevel'], minAccessLevel) if str(userId) not in self.dbm: data['accountId'] = 0 else: data['accountId'] = int(self.dbm[str(userId)]) callback(data) return data def storeAccountID(self, userId, accountId, callback): self.dbm[str(userId)] = str(accountId) # anydbm only allows strings. if getattr(self.dbm, 'sync', None): self.dbm.sync() callback(True) else: self.notify.warning('Unable to associate user %s with account %d!' % (userId, accountId)) callback(False) class DeveloperAccountDB(AccountDB): notify = directNotify.newCategory('DeveloperAccountDB') def lookup(self, userId, callback): return AccountDB.lookup(self, {'userId': userId, 'accessLevel': 700, 'notAfter': 0}, callback) class RemoteAccountDB: # TO DO FOR NAMES: # CURRENTLY IT MAKES n REQUESTS FOR EACH AVATAR # IN THE FUTURE, MAKE ONLY 1 REQUEST # WHICH RETURNS ALL PENDING AVS # ^ done, check before removing todo note notify = directNotify.newCategory('RemoteAccountDB') def __init__(self, csm): self.csm = csm def addNameRequest(self, avId, name, accountID = None): username = avId if accountID is not None: username = accountID res = executeHttpRequest('names', action='set', username=str(username), avId=str(avId), wantedName=name) if res is not None: return True return False def getNameStatus(self, accountId, callback = None): r = executeHttpRequest('names', action='get', username=str(accountId)) try: r = json.loads(r) if callback is not None: callback(r) return True except: return False def removeNameRequest(self, avId): r = executeHttpRequest('names', action='del', avId=str(avId)) if r: return 'SUCCESS' return 'FAILURE' def lookup(self, token, callback): ''' Token format: The token is obfuscated a bit, but nothing too hard to read. Most of the security is based on the hash. I. Data contained in a token: A json-encoded dict, which contains timestamp, userid and extra info II. Token format X = BASE64(ROT13(DATA)[::-1]) H = HASH(X)[::-1] Token = BASE64(H + X) ''' cookie_check = executeHttpRequest('cookie', cookie=token) try: check = json.loads(cookie_check) if check['success'] is not True: raise ValueError(check['error']) token = token.decode('base64') hash, token = token[:hashSize], token[hashSize:] correctHash = hashAlgo(token + accountServerSecret).digest() if len(hash) != len(correctHash): raise ValueError('Invalid hash.') value = 0 for x, y in zip(hash[::-1], correctHash): value |= ord(x) ^ ord(y) if value: raise ValueError('Invalid hash.') token = json.loads(token.decode('base64')[::-1].decode('rot13')) if token['notAfter'] < int(time.time()): raise ValueError('Expired token.') except: resp = {'success': False} callback(resp) return resp return self.account_lookup(token, callback) def account_lookup(self, data, callback): data['success'] = True data['accessLevel'] = max(data['accessLevel'], minAccessLevel) data['accountId'] = int(data['accountId']) callback(data) return data def storeAccountID(self, userId, accountId, callback): r = executeHttpRequest('associateuser', username=str(userId), accountId=str(accountId)) try: r = json.loads(r) if r['success']: callback(True) else: self.notify.warning('Unable to associate user %s with account %d, got the return message of %s!' % (userId, accountId, r['error'])) callback(False) except: self.notify.warning('Unable to associate user %s with account %d!' % (userId, accountId)) callback(False) # --- FSMs --- class OperationFSM(FSM): TARGET_CONNECTION = False def __init__(self, csm, target): self.csm = csm self.target = target FSM.__init__(self, self.__class__.__name__) def enterKill(self, reason=''): if self.TARGET_CONNECTION: self.csm.killConnection(self.target, reason) else: self.csm.killAccount(self.target, reason) self.demand('Off') def enterOff(self): if self.TARGET_CONNECTION: del self.csm.connection2fsm[self.target] else: del self.csm.account2fsm[self.target] class LoginAccountFSM(OperationFSM): notify = directNotify.newCategory('LoginAccountFSM') TARGET_CONNECTION = True def enterStart(self, token): self.token = token self.demand('QueryAccountDB') def enterQueryAccountDB(self): self.csm.accountDB.lookup(self.token, self.__handleLookup) def __handleLookup(self, result): if not result.get('success'): self.csm.air.writeServerEvent('tokenRejected', self.target, self.token) self.demand('Kill', result.get('reason', 'The account server rejected your token.')) return self.userId = result.get('userId', 0) self.accountId = result.get('accountId', 0) self.accessLevel = result.get('accessLevel', 0) self.notAfter = result.get('notAfter', 0) if self.accountId: self.demand('RetrieveAccount') else: self.demand('CreateAccount') def enterRetrieveAccount(self): self.csm.air.dbInterface.queryObject( self.csm.air.dbId, self.accountId, self.__handleRetrieve) def __handleRetrieve(self, dclass, fields): if dclass != self.csm.air.dclassesByName['AccountUD']: self.demand('Kill', 'Your account object was not found in the database!') return self.account = fields if self.notAfter: if self.account.get('LAST_LOGIN_TS', 0) > self.notAfter: self.notify.debug('Rejecting old token: %d, notAfter=%d' % (self.account.get('LAST_LOGIN_TS', 0), self.notAfter)) return self.__handleLookup({'success': False}) self.demand('SetAccount') def enterCreateAccount(self): self.account = { 'ACCOUNT_AV_SET': [0] * 6, 'ESTATE_ID': 0, 'ACCOUNT_AV_SET_DEL': [], 'CREATED': time.ctime(), 'LAST_LOGIN': time.ctime(), 'LAST_LOGIN_TS': time.time(), 'ACCOUNT_ID': str(self.userId), 'ACCESS_LEVEL': self.accessLevel, 'CHAT_SETTINGS': [1, 1] } self.csm.air.dbInterface.createObject( self.csm.air.dbId, self.csm.air.dclassesByName['AccountUD'], self.account, self.__handleCreate) def __handleCreate(self, accountId): if self.state != 'CreateAccount': self.notify.warning('Received a create account response outside of the CreateAccount state.') return if not accountId: self.notify.warning('Database failed to construct an account object!') self.demand('Kill', 'Your account object could not be created in the game database.') return self.accountId = accountId self.csm.air.writeServerEvent('accountCreated', accountId) self.demand('StoreAccountID') def enterStoreAccountID(self): self.csm.accountDB.storeAccountID( self.userId, self.accountId, self.__handleStored) def __handleStored(self, success=True): if not success: self.demand('Kill', 'The account server could not save your user ID!') return self.demand('SetAccount') def enterSetAccount(self): # If necessary, update their account information: if self.accessLevel: self.csm.air.dbInterface.updateObject( self.csm.air.dbId, self.accountId, self.csm.air.dclassesByName['AccountUD'], {'ACCESS_LEVEL': self.accessLevel}) # If there's anybody on the account, kill them for redundant login: datagram = PyDatagram() datagram.addServerHeader( self.csm.GetAccountConnectionChannel(self.accountId), self.csm.air.ourChannel, CLIENTAGENT_EJECT) datagram.addUint16(100) datagram.addString('This account has been logged in from elsewhere.') self.csm.air.send(datagram) # Next, add this connection to the account channel. datagram = PyDatagram() datagram.addServerHeader( self.target, self.csm.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL) datagram.addChannel(self.csm.GetAccountConnectionChannel(self.accountId)) self.csm.air.send(datagram) # Subscribe to any "staff" channels that the account has access to. access = self.account.get('ADMIN_ACCESS', 0) if access >= 200: # Subscribe to the moderator channel. dg = PyDatagram() dg.addServerHeader(self.target, self.csm.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL) dg.addChannel(OtpDoGlobals.OTP_MOD_CHANNEL) self.csm.air.send(dg) if access >= 400: # Subscribe to the administrator channel. dg = PyDatagram() dg.addServerHeader(self.target, self.csm.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL) dg.addChannel(OtpDoGlobals.OTP_ADMIN_CHANNEL) self.csm.air.send(dg) if access >= 500: # Subscribe to the system administrator channel. dg = PyDatagram() dg.addServerHeader(self.target, self.csm.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL) dg.addChannel(OtpDoGlobals.OTP_SYSADMIN_CHANNEL) self.csm.air.send(dg) # Now set their sender channel to represent their account affiliation: datagram = PyDatagram() datagram.addServerHeader( self.target, self.csm.air.ourChannel, CLIENTAGENT_SET_CLIENT_ID) # Account ID in high 32 bits, 0 in low (no avatar): datagram.addChannel(self.accountId << 32) self.csm.air.send(datagram) # Un-sandbox them! datagram = PyDatagram() datagram.addServerHeader( self.target, self.csm.air.ourChannel, CLIENTAGENT_SET_STATE) datagram.addUint16(2) # ESTABLISHED self.csm.air.send(datagram) # Update the last login timestamp: self.csm.air.dbInterface.updateObject( self.csm.air.dbId, self.accountId, self.csm.air.dclassesByName['AccountUD'], {'LAST_LOGIN': time.ctime(), 'LAST_LOGIN_TS': time.time(), 'ACCOUNT_ID': str(self.userId)}) # We're done. self.csm.air.writeServerEvent('accountLogin', self.target, self.accountId, self.userId) self.csm.sendUpdateToChannel(self.target, 'acceptLogin', [int(time.time())]) self.demand('Off') class CreateAvatarFSM(OperationFSM): notify = directNotify.newCategory('CreateAvatarFSM') def enterStart(self, dna, index): # Basic sanity-checking: if index >= 6: self.demand('Kill', 'Invalid index specified!') return if not ToonDNA().isValidNetString(dna): self.demand('Kill', 'Invalid DNA specified!') return self.index = index self.dna = dna # Okay, we're good to go, let's query their account. self.demand('RetrieveAccount') def enterRetrieveAccount(self): self.csm.air.dbInterface.queryObject( self.csm.air.dbId, self.target, self.__handleRetrieve) def __handleRetrieve(self, dclass, fields): if dclass != self.csm.air.dclassesByName['AccountUD']: self.demand('Kill', 'Your account object was not found in the database!') return self.account = fields # For use in calling name requests: self.accountID = self.account['ACCOUNT_ID'] self.avList = self.account['ACCOUNT_AV_SET'] # Sanitize: self.avList = self.avList[:6] self.avList += [0] * (6-len(self.avList)) # Make sure the index is open: if self.avList[self.index]: self.demand('Kill', 'This avatar slot is already taken by another avatar!') return # Okay, there's space. Let's create the avatar! self.demand('CreateAvatar') def enterCreateAvatar(self): dna = ToonDNA() dna.makeFromNetString(self.dna) colorString = TTLocalizer.ColorfulToon animalType = TTLocalizer.AnimalToSpecies[dna.getAnimal()] name = ' '.join((colorString, animalType)) toonFields = { 'setName': (name,), 'setWishNameState': ('OPEN',), 'setWishName': ('',), 'setDNAString': (self.dna,), 'setDISLid': (self.target,), } self.csm.air.dbInterface.createObject( self.csm.air.dbId, self.csm.air.dclassesByName['DistributedToonUD'], toonFields, self.__handleCreate) def __handleCreate(self, avId): if not avId: self.demand('Kill', 'Database failed to create the new avatar object!') return self.avId = avId self.demand('StoreAvatar') def enterStoreAvatar(self): # Associate the avatar with the account... self.avList[self.index] = self.avId self.csm.air.dbInterface.updateObject( self.csm.air.dbId, self.target, self.csm.air.dclassesByName['AccountUD'], {'ACCOUNT_AV_SET': self.avList}, {'ACCOUNT_AV_SET': self.account['ACCOUNT_AV_SET']}, self.__handleStoreAvatar) self.accountID = self.account['ACCOUNT_ID'] def __handleStoreAvatar(self, fields): if fields: self.demand('Kill', 'Database failed to associate the new avatar to your account!') return # Otherwise, we're done! self.csm.air.writeServerEvent('avatarCreated', self.avId, self.target, self.dna.encode('hex'), self.index) self.csm.sendUpdateToAccountId(self.target, 'createAvatarResp', [self.avId]) self.demand('Off') class AvatarOperationFSM(OperationFSM): POST_ACCOUNT_STATE = 'Off' # This needs to be overridden. def enterRetrieveAccount(self): # Query the account: self.csm.air.dbInterface.queryObject( self.csm.air.dbId, self.target, self.__handleRetrieve) def __handleRetrieve(self, dclass, fields): if dclass != self.csm.air.dclassesByName['AccountUD']: self.demand('Kill', 'Your account object was not found in the database!') return self.account = fields # For use in calling name requests: self.accountID = self.account['ACCOUNT_ID'] self.avList = self.account['ACCOUNT_AV_SET'] # Sanitize: self.avList = self.avList[:6] self.avList += [0] * (6-len(self.avList)) self.demand(self.POST_ACCOUNT_STATE) class GetAvatarsFSM(AvatarOperationFSM): notify = directNotify.newCategory('GetAvatarsFSM') POST_ACCOUNT_STATE = 'QueryAvatars' def enterStart(self): self.demand('RetrieveAccount') self.nameStateData = None def enterQueryAvatars(self): self.pendingAvatars = set() self.avatarFields = {} for avId in self.avList: if avId: self.pendingAvatars.add(avId) def response(dclass, fields, avId=avId): if self.state != 'QueryAvatars': return if dclass != self.csm.air.dclassesByName['DistributedToonUD']: self.demand('Kill', "One of the account's avatars is invalid!") return self.avatarFields[avId] = fields self.pendingAvatars.remove(avId) if not self.pendingAvatars: self.demand('SendAvatars') self.csm.air.dbInterface.queryObject( self.csm.air.dbId, avId, response) if not self.pendingAvatars: self.demand('SendAvatars') def enterSendAvatars(self): potentialAvs = [] for avId, fields in self.avatarFields.items(): index = self.avList.index(avId) wishNameState = fields.get('setWishNameState', [''])[0] name = fields['setName'][0] nameState = 0 if wishNameState == 'OPEN': nameState = 1 elif wishNameState == 'PENDING': if accountDBType == 'remote': if self.nameStateData is None: self.demand('QueryNameState') return actualNameState = self.nameStateData[str(avId)] else: actualNameState = self.csm.accountDB.getNameStatus(self.account['ACCOUNT_ID']) self.csm.air.dbInterface.updateObject( self.csm.air.dbId, avId, self.csm.air.dclassesByName['DistributedToonUD'], {'setWishNameState': [actualNameState]} ) if actualNameState == 'PENDING': nameState = 2 if actualNameState == 'APPROVED': nameState = 3 name = fields['setWishName'][0] elif actualNameState == 'REJECTED': nameState = 4 elif wishNameState == 'APPROVED': nameState = 3 elif wishNameState == 'REJECTED': nameState = 4 potentialAvs.append([avId, name, fields['setDNAString'][0], index, nameState]) self.csm.sendUpdateToAccountId(self.target, 'setAvatars', [self.account['CHAT_SETTINGS'], potentialAvs]) self.demand('Off') def enterQueryNameState(self): def gotStates(data): self.nameStateData = data taskMgr.doMethodLater(0, GetAvatarsFSM.demand, 'demand-QueryAvatars', extraArgs=[self, 'QueryAvatars']) self.csm.accountDB.getNameStatus(self.account['ACCOUNT_ID'], gotStates) # We should've called the taskMgr action by now. # This inherits from GetAvatarsFSM, because the delete operation ends in a # setAvatars message being sent to the client. class DeleteAvatarFSM(GetAvatarsFSM): notify = directNotify.newCategory('DeleteAvatarFSM') POST_ACCOUNT_STATE = 'ProcessDelete' def enterStart(self, avId): self.avId = avId GetAvatarsFSM.enterStart(self) def enterProcessDelete(self): if self.avId not in self.avList: self.demand('Kill', 'Tried to delete an avatar not in the account!') return index = self.avList.index(self.avId) self.avList[index] = 0 avsDeleted = list(self.account.get('ACCOUNT_AV_SET_DEL', [])) avsDeleted.append([self.avId, int(time.time())]) estateId = self.account.get('ESTATE_ID', 0) if estateId != 0: # This assumes that the house already exists, but it shouldn't # be a problem if it doesn't. self.csm.air.dbInterface.updateObject( self.csm.air.dbId, estateId, self.csm.air.dclassesByName['DistributedEstateAI'], {'setSlot%dToonId' % index: [0], 'setSlot%dGarden' % index: [[]]} ) self.csm.air.dbInterface.updateObject( self.csm.air.dbId, self.target, self.csm.air.dclassesByName['AccountUD'], {'ACCOUNT_AV_SET': self.avList, 'ACCOUNT_AV_SET_DEL': avsDeleted}, {'ACCOUNT_AV_SET': self.account['ACCOUNT_AV_SET'], 'ACCOUNT_AV_SET_DEL': self.account['ACCOUNT_AV_SET_DEL']}, self.__handleDelete) self.csm.accountDB.removeNameRequest(self.avId) def __handleDelete(self, fields): if fields: self.demand('Kill', 'Database failed to mark the avatar as deleted!') return self.csm.air.friendsManager.clearList(self.avId) self.csm.air.writeServerEvent('avatarDeleted', self.avId, self.target) self.demand('QueryAvatars') class SetNameTypedFSM(AvatarOperationFSM): notify = directNotify.newCategory('SetNameTypedFSM') POST_ACCOUNT_STATE = 'RetrieveAvatar' def enterStart(self, avId, name): self.avId = avId self.name = name self.set_account_id = None if self.avId: self.demand('RetrieveAccount') return # Hmm, self.avId was 0. Okay, let's just cut to the judging: self.demand('JudgeName') def enterRetrieveAvatar(self): if self.accountID: self.set_account_id = self.accountID if self.avId and self.avId not in self.avList: self.demand('Kill', 'Tried to name an avatar not in the account!') return self.csm.air.dbInterface.queryObject(self.csm.air.dbId, self.avId, self.__handleAvatar) def __handleAvatar(self, dclass, fields): if dclass != self.csm.air.dclassesByName['DistributedToonUD']: self.demand('Kill', "One of the account's avatars is invalid!") return if fields['setWishNameState'][0] != 'OPEN': self.demand('Kill', 'Avatar is not in a namable state!') return self.demand('JudgeName') def enterJudgeName(self): # Let's see if the name is valid: status = judgeName(self.name) if self.avId and status: if self.csm.accountDB.addNameRequest(self.avId, self.name, accountID=self.set_account_id): self.csm.air.dbInterface.updateObject( self.csm.air.dbId, self.avId, self.csm.air.dclassesByName['DistributedToonUD'], {'setWishNameState': ('PENDING',), 'setWishName': (self.name,)}) else: status = False if self.avId: self.csm.air.writeServerEvent('avatarWishname', self.avId, self.name) self.csm.sendUpdateToAccountId(self.target, 'setNameTypedResp', [self.avId, status]) self.demand('Off') class SetNamePatternFSM(AvatarOperationFSM): notify = directNotify.newCategory('SetNamePatternFSM') POST_ACCOUNT_STATE = 'RetrieveAvatar' def enterStart(self, avId, pattern): self.avId = avId self.pattern = pattern self.demand('RetrieveAccount') def enterRetrieveAvatar(self): if self.avId and self.avId not in self.avList: self.demand('Kill', 'Tried to name an avatar not in the account!') return self.csm.air.dbInterface.queryObject(self.csm.air.dbId, self.avId, self.__handleAvatar) def __handleAvatar(self, dclass, fields): if dclass != self.csm.air.dclassesByName['DistributedToonUD']: self.demand('Kill', "One of the account's avatars is invalid!") return if fields['setWishNameState'][0] != 'OPEN': self.demand('Kill', 'Avatar is not in a namable state!') return self.demand('SetName') def enterSetName(self): # Render the pattern into a string: parts = [] for p, f in self.pattern: part = self.csm.nameGenerator.nameDictionary.get(p, ('', ''))[1] if f: part = part[:1].upper() + part[1:] else: part = part.lower() parts.append(part) parts[2] += parts.pop(3) # Merge 2&3 (the last name) as there should be no space. while '' in parts: parts.remove('') name = ' '.join(parts) self.csm.air.dbInterface.updateObject( self.csm.air.dbId, self.avId, self.csm.air.dclassesByName['DistributedToonUD'], {'setWishNameState': ('',), 'setWishName': ('',), 'setName': (name,)}) self.csm.air.writeServerEvent('avatarNamed', self.avId, name) self.csm.sendUpdateToAccountId(self.target, 'setNamePatternResp', [self.avId, 1]) self.demand('Off') class AcknowledgeNameFSM(AvatarOperationFSM): notify = directNotify.newCategory('AcknowledgeNameFSM') POST_ACCOUNT_STATE = 'GetTargetAvatar' def enterStart(self, avId): self.avId = avId self.demand('RetrieveAccount') def enterGetTargetAvatar(self): # Make sure the target avatar is part of the account: if self.avId not in self.avList: self.demand('Kill', 'Tried to acknowledge name on an avatar not in the account!') return self.csm.air.dbInterface.queryObject(self.csm.air.dbId, self.avId, self.__handleAvatar) def __handleAvatar(self, dclass, fields): if dclass != self.csm.air.dclassesByName['DistributedToonUD']: self.demand('Kill', "One of the account's avatars is invalid!") return # Process the WishNameState change. wishNameState = fields['setWishNameState'][0] wishName = fields['setWishName'][0] name = fields['setName'][0] if wishNameState == 'APPROVED': wishNameState = '' name = wishName wishName = '' self.csm.accountDB.removeNameRequest(self.avId) elif wishNameState == 'REJECTED': wishNameState = 'OPEN' wishName = '' self.csm.accountDB.removeNameRequest(self.avId) else: self.demand('Kill', "Tried to acknowledge name on an avatar in %s state!" % wishNameState) return # Push the change back through: self.csm.air.dbInterface.updateObject( self.csm.air.dbId, self.avId, self.csm.air.dclassesByName['DistributedToonUD'], {'setWishNameState': (wishNameState,), 'setWishName': (wishName,), 'setName': (name,)}, {'setWishNameState': fields['setWishNameState'], 'setWishName': fields['setWishName'], 'setName': fields['setName']}) self.csm.sendUpdateToAccountId(self.target, 'acknowledgeAvatarNameResp', []) self.demand('Off') class LoadAvatarFSM(AvatarOperationFSM): notify = directNotify.newCategory('LoadAvatarFSM') POST_ACCOUNT_STATE = 'GetTargetAvatar' def enterStart(self, avId): self.avId = avId self.demand('RetrieveAccount') def enterGetTargetAvatar(self): # Make sure the target avatar is part of the account: if self.avId not in self.avList: self.demand('Kill', 'Tried to play an avatar not in the account!') return self.csm.air.dbInterface.queryObject(self.csm.air.dbId, self.avId, self.__handleAvatar) def __handleAvatar(self, dclass, fields): if dclass != self.csm.air.dclassesByName['DistributedToonUD']: self.demand('Kill', "One of the account's avatars is invalid!") return self.avatar = fields self.demand('SetAvatar') def enterSetAvatarTask(self, channel, task): # Finally, grant ownership and shut down. datagram = PyDatagram() datagram.addServerHeader( self.avId, self.csm.air.ourChannel, STATESERVER_OBJECT_SET_OWNER) datagram.addChannel(self.target<<32 | self.avId) self.csm.air.send(datagram) # Tell the GlobalPartyManager as well: self.csm.air.globalPartyMgr.avatarJoined(self.avId) fields = self.avatar fields.update({'setAdminAccess': [self.account.get('ACCESS_LEVEL', 100)]}) self.csm.air.friendsManager.addToonData(self.avId, fields) self.csm.air.writeServerEvent('avatarChosen', self.avId, self.target) self.demand('Off') return task.done def enterSetAvatar(self): channel = self.csm.GetAccountConnectionChannel(self.target) # First, give them a POSTREMOVE to unload the avatar, just in case they # disconnect while we're working. datagramCleanup = PyDatagram() datagramCleanup.addServerHeader( self.avId, channel, STATESERVER_OBJECT_DELETE_RAM) datagramCleanup.addUint32(self.avId) datagram = PyDatagram() datagram.addServerHeader( channel, self.csm.air.ourChannel, CLIENTAGENT_ADD_POST_REMOVE) datagram.addString(datagramCleanup.getMessage()) self.csm.air.send(datagram) # Activate the avatar on the DBSS: self.csm.air.sendActivate( self.avId, 0, 0, self.csm.air.dclassesByName['DistributedToonUD'], {'setAdminAccess': [self.account.get('ACCESS_LEVEL', 100)]}) # Next, add them to the avatar channel: datagram = PyDatagram() datagram.addServerHeader( channel, self.csm.air.ourChannel, CLIENTAGENT_OPEN_CHANNEL) datagram.addChannel(self.csm.GetPuppetConnectionChannel(self.avId)) self.csm.air.send(datagram) # Now set their sender channel to represent their account affiliation: datagram = PyDatagram() datagram.addServerHeader( channel, self.csm.air.ourChannel, CLIENTAGENT_SET_CLIENT_ID) datagram.addChannel(self.target<<32 | self.avId) self.csm.air.send(datagram) # Eliminate race conditions. taskMgr.doMethodLater(0.2, self.enterSetAvatarTask, 'avatarTask-%s' % self.avId, extraArgs=[channel], appendTask=True) class UnloadAvatarFSM(OperationFSM): notify = directNotify.newCategory('UnloadAvatarFSM') def enterStart(self, avId): self.avId = avId # We don't even need to query the account, we know the avatar is being played! self.demand('UnloadAvatar') def enterUnloadAvatar(self): channel = self.csm.GetAccountConnectionChannel(self.target) # Tell TTSFriendsManager somebody is logging off: self.csm.air.friendsManager.toonOffline(self.avId) # Clear off POSTREMOVE: datagram = PyDatagram() datagram.addServerHeader( channel, self.csm.air.ourChannel, CLIENTAGENT_CLEAR_POST_REMOVES) self.csm.air.send(datagram) # Remove avatar channel: datagram = PyDatagram() datagram.addServerHeader( channel, self.csm.air.ourChannel, CLIENTAGENT_CLOSE_CHANNEL) datagram.addChannel(self.csm.GetPuppetConnectionChannel(self.avId)) self.csm.air.send(datagram) # Reset sender channel: datagram = PyDatagram() datagram.addServerHeader( channel, self.csm.air.ourChannel, CLIENTAGENT_SET_CLIENT_ID) datagram.addChannel(self.target<<32) self.csm.air.send(datagram) # Unload avatar object: datagram = PyDatagram() datagram.addServerHeader( self.avId, channel, STATESERVER_OBJECT_DELETE_RAM) datagram.addUint32(self.avId) self.csm.air.send(datagram) # Done! self.csm.air.writeServerEvent('avatarUnload', self.avId) self.demand('Off') # --- CLIENT SERVICES MANAGER UBERDOG --- class ClientServicesManagerUD(DistributedObjectGlobalUD): notify = directNotify.newCategory('ClientServicesManagerUD') def announceGenerate(self): DistributedObjectGlobalUD.announceGenerate(self) # These keep track of the connection/account IDs currently undergoing an # operation on the CSM. This is to prevent (hacked) clients from firing up more # than one operation at a time, which could potentially lead to exploitation # of race conditions. self.connection2fsm = {} self.account2fsm = {} # For processing name patterns. self.nameGenerator = NameGenerator() # Temporary HMAC key: self.key = 'c603c5833021ce79f734943f6e662250fd4ecf7432bf85905f71707dc4a9370c6ae15a8716302ead43810e5fba3cf0876bbbfce658e2767b88d916f5d89fd31' # Instantiate our account DB interface: if accountDBType == 'developer': self.accountDB = DeveloperAccountDB(self) elif accountDBType == 'remote': self.accountDB = RemoteAccountDB(self) else: self.notify.error('Invalid accountdb-type: ' + accountDBType) def killConnection(self, connId, reason): datagram = PyDatagram() datagram.addServerHeader( connId, self.air.ourChannel, CLIENTAGENT_EJECT) datagram.addUint16(101) datagram.addString(reason) self.air.send(datagram) def killConnectionFSM(self, connId): fsm = self.connection2fsm.get(connId) if not fsm: self.notify.warning('Tried to kill connection %d for duplicate FSM, but none exists!' % connId) return self.killConnection(connId, 'An operation is already underway: ' + fsm.name) def killAccount(self, accountId, reason): self.killConnection(self.GetAccountConnectionChannel(accountId), reason) def killAccountFSM(self, accountId): fsm = self.account2fsm.get(accountId) if not fsm: self.notify.warning('Tried to kill account %d for duplicate FSM, but none exists!' % accountId) return self.killAccount(accountId, 'An operation is already underway: ' + fsm.name) def runAccountFSM(self, fsmtype, *args): sender = self.air.getAccountIdFromSender() if not sender: self.killAccount(sender, 'Client is not logged in.') if sender in self.account2fsm: self.killAccountFSM(sender) return self.account2fsm[sender] = fsmtype(self, sender) self.account2fsm[sender].request('Start', *args) def login(self, cookie, authKey): self.notify.debug('Received login cookie %r from %d' % (cookie, self.air.getMsgSender())) sender = self.air.getMsgSender() # Time to check this login to see if its authentic digest_maker = hmac.new(self.key) digest_maker.update(cookie) serverKey = digest_maker.hexdigest() if serverKey == authKey: # This login is authentic! pass else: # This login is not authentic. self.killConnection(sender, ' ') if sender >> 32: self.killConnection(sender, 'Client is already logged in.') return if sender in self.connection2fsm: self.killConnectionFSM(sender) return self.connection2fsm[sender] = LoginAccountFSM(self, sender) self.connection2fsm[sender].request('Start', cookie) def requestAvatars(self): self.notify.debug('Received avatar list request from %d' % (self.air.getMsgSender())) self.runAccountFSM(GetAvatarsFSM) def createAvatar(self, dna, index): self.runAccountFSM(CreateAvatarFSM, dna, index) def deleteAvatar(self, avId): self.runAccountFSM(DeleteAvatarFSM, avId) def setNameTyped(self, avId, name): self.runAccountFSM(SetNameTypedFSM, avId, name) def setNamePattern(self, avId, p1, f1, p2, f2, p3, f3, p4, f4): self.runAccountFSM(SetNamePatternFSM, avId, [(p1, f1), (p2, f2), (p3, f3), (p4, f4)]) def acknowledgeAvatarName(self, avId): self.runAccountFSM(AcknowledgeNameFSM, avId) def chooseAvatar(self, avId): currentAvId = self.air.getAvatarIdFromSender() accountId = self.air.getAccountIdFromSender() if currentAvId and avId: self.killAccount(accountId, 'A Toon is already chosen!') return elif not currentAvId and not avId: # This isn't really an error, the client is probably just making sure # none of its Toons are active. return if avId: self.runAccountFSM(LoadAvatarFSM, avId) else: self.runAccountFSM(UnloadAvatarFSM, currentAvId)