def find_player_nopw(self, handler, name): """ Locate the player whose name *or* email address is given. Return the player dict. If the player is not found, return None. This is the same as above, but doesn't require a password. We use this only for password recovery. """ if (not self.app.mongodb): raise MessageException('Database not available.') if ('@' in name): key = 'email' else: key = 'name' try: res = yield motor.Op(self.app.mongodb.players.find_one, {key: name}) except Exception as ex: raise MessageException('Database error: %s' % ex) if not res: return None return res
def find_player(self, handler, name, password): """ Locate the player whose name *or* email address is given. Return the player dict. If the password does not match, or the player is not found, return None. This presumes that names do not contain @ signs, and that neither names nor email addresses are duplicated. """ if (not self.app.mongodb): raise MessageException('Database not available.') if ('@' in name): key = 'email' else: key = 'name' try: res = yield motor.Op(self.app.mongodb.players.find_one, {key: name}) except Exception as ex: raise MessageException('Database error: %s' % ex) if not res: return None # Check password. (It is already a bytes.) saltedpw = res['pwsalt'] + b':' + password cryptpw = hashlib.sha1(saltedpw).hexdigest().encode() if (res['password'] != cryptpw): return None return res
def create_session(self, handler, uid, email, name): """ Create a session from the request parameters. Return the session id. """ if (not self.app.mongodb): raise MessageException('Database not available') # Generate a random sessionid. sessionid = self.random_bytes(24) handler.set_secure_cookie('sessionid', sessionid, expires_days=10) now = twcommon.misc.now() sess = { 'sid': sessionid, 'uid': uid, 'email': email, 'name': name, 'ipaddr': handler.request.remote_ip, 'starttime': now, 'refreshtime': now, } res = yield motor.Op(self.app.mongodb.sessions.insert, sess) return sessionid
def get_config_key(self, key): """ Look up a config key in the database. If not present, return None. """ try: res = yield motor.Op(self.application.mongodb.config.find_one, { 'key': key }) except Exception as ex: raise MessageException('Database error: %s' % (ex,)) if not res: return None return res['val']
def find_current_session(self): """ Look up the user's session, using the sessionid cookie. Sets twsessionstatus to be 'auth', 'unauth', or 'unknown' (if the auth server is unavailable). In the 'auth' case, also sets twsession to the session dict. If this is never called (e.g., the error handler) then the status remains None. This method should catch all its own exceptions (setting 'unknown'). This is invoked from the prepare() method, but also manually if we know the session info has changed. """ if self.application.caughtinterrupt: # Server is shutting down; don't accept any significant requests. raise MessageException('Server is shutting down!') res = yield self.application.twsessionmgr.find_session(self) if (res): (self.twsessionstatus, self.twsession) = res return True
def create_session_guest(self, handler): """ Create a guest session. Return the session id and email address, or raise an exception. """ # Find the first guest account which is not in use. player = None cursor = self.app.mongodb.players.find({'guest': True}, sort=[('_id', 1)]) while (yield cursor.fetch_next): res = cursor.next_object() if res.get('guestsession', None): # This one is busy (or being cleaned up). continue player = res break yield motor.Op(cursor.close) if not player: raise MessageException( 'All guest accounts are busy right now! You can still register a permanent account.' ) uid = player['_id'] # Generate a random sessionid. sessionid = self.random_bytes(24) handler.set_secure_cookie('sessionid', sessionid, expires_days=10) # Mark the guest account as in-use, and clear out its associated # data. (Including desc and pronoun.) playerfields = yield motor.Op(self.app.mongodb.config.find_one, {'key': 'playerfields'}) if not playerfields: raise Exception('No playerfields data found for guest!') playerfields = playerfields['val'] playerfields['build'] = False playerfields['askbuild'] = False playerfields['guestsession'] = sessionid yield motor.Op(self.app.mongodb.players.update, {'_id': uid}, {'$set': playerfields}) # Create the first entry for the player's personal portlist. try: yield self.create_starting_portal(player['plistid'], player['scid']) except Exception as ex: self.app.twlog.error('Error creating guest\'s first portal: %s', ex) # Finally, create the session entry. now = twcommon.misc.now() sess = { 'sid': sessionid, 'uid': uid, 'email': player['email'], 'name': player['name'], 'ipaddr': handler.request.remote_ip, 'starttime': now, 'refreshtime': now, 'guest': True, } res = yield motor.Op(self.app.mongodb.sessions.insert, sess) return (sessionid, player['email'])
def create_player(self, handler, email, name, password): """ Create a player entry with the given parameters. Also create a session and sign the player in. The name and email should already have been validated and canonicalized, as much as possible. """ if (not self.app.mongodb): raise MessageException('Database not available.') namekey = sluggify(name) # Check for collisions first. try: resname = yield motor.Op(self.app.mongodb.players.find_one, {'name': name}) resnamekey = yield motor.Op(self.app.mongodb.players.find_one, {'namekey': namekey}) resemail = yield motor.Op(self.app.mongodb.players.find_one, {'email': email}) except Exception as ex: raise MessageException('Database error: %s' % ex) if (resname): raise MessageException('The player name %s is already in use.' % (name, )) if (resnamekey): raise MessageException('The player name %s is already in use.' % (resnamekey['name'], )) if (resemail): raise MessageException('That email address is already registered.') # Both the salt and password strings are stored as bytes, although # they'll really be ascii hex digits. pwsalt = self.random_bytes(8) saltedpw = pwsalt + b':' + password cryptpw = hashlib.sha1(saltedpw).hexdigest().encode() player = { 'name': name, 'namekey': namekey, 'email': email, 'pwsalt': pwsalt, 'password': cryptpw, 'createtime': twcommon.misc.now(), } playerfields = yield motor.Op(self.app.mongodb.config.find_one, {'key': 'playerfields'}) if playerfields: player.update(playerfields['val']) uid = yield motor.Op(self.app.mongodb.players.insert, player) if not uid: raise MessageException('Unable to create player.') # Create the playstate entry. playstate = { '_id': uid, 'iid': None, 'locid': None, 'focus': None, } uid = yield motor.Op(self.app.mongodb.playstate.insert, playstate) if not uid: raise MessageException('Unable to create playstate.') # Create a personal scope for the player. scope = { 'type': 'pers', 'uid': uid, } scid = yield motor.Op(self.app.mongodb.scopes.insert, scope) yield motor.Op(self.app.mongodb.players.update, {'_id': uid}, {'$set': { 'scid': scid }}) # And give the player full access to it yield motor.Op(self.app.mongodb.scopeaccess.insert, { 'uid': uid, 'scid': scid, 'level': twcommon.access.ACC_FOUNDER }) # Create a personal portlist (booklet) for the player. portlist = { 'type': 'pers', 'uid': uid, } plistid = yield motor.Op(self.app.mongodb.portlists.insert, portlist) yield motor.Op(self.app.mongodb.players.update, {'_id': uid}, {'$set': { 'plistid': plistid }}) # Create the first entry for the portlist. try: yield self.create_starting_portal(plistid, scid) except Exception as ex: self.app.twlog.error('Error creating player\'s first portal: %s', ex) # Create a sign-in session too, and we're done. sessionid = yield tornado.gen.Task(self.create_session, handler, uid, email, name) return sessionid
def send(self, toaddr, subject, body): """Send a message. Raises an exception on failure. The From line is taken from the application config. """ mailargs = self.app.twopts.email_command if not mailargs: raise MessageException( 'Unable to send recovery email -- email command not configured.' ) mailargs = shlex.split(mailargs) replace_array_el(mailargs, '$TO', toaddr) replace_array_el(mailargs, '$FROM', self.app.twopts.email_from) replace_array_el(mailargs, '$SUBJECT', subject) proc = tornado.process.Subprocess( mailargs, close_fds=True, stdin=tornado.process.Subprocess.STREAM, stdout=tornado.process.Subprocess.STREAM) # We'll read from the subprocess, logging all output, # and triggering a callback when its stdout closes. callkey = ObjectId() # unique key proc.stdout.read_until_close( (yield tornado.gen.Callback(callkey)), lambda dat: self.log.info('Email script output: %s', dat)) # Now push in the message body. proc.stdin.write(body, callback=proc.stdin.close) proc.stdin.close() # And wait for that close callback. yield tornado.gen.Wait(callkey) # Wait a few more seconds for the process to exit, which it should. # (stdout has closed, so it's done.) # This is probably terrible use of the ioloop, but I don't want # to rely on set_exit_callback and its SIGCHILD weirdness. callkey = ObjectId() # unique key callback = yield tornado.gen.Callback(callkey) ticker = list(range(8)) tickdelta = datetime.timedelta(seconds=0.25) ioloop = tornado.ioloop.IOLoop.instance() def func(): if proc.proc.poll() is not None: # process has exited callback() elif not ticker: # out of ticks callback() else: # reduce the ticker, call again soon ticker.pop() ioloop.add_timeout(tickdelta, func) ioloop.add_callback(func) yield tornado.gen.Wait(callkey) res = proc.proc.poll() self.log.info('Email script result: %s', res) if res is None: raise MessageException('Email sending timed out.') if res: raise MessageException('Email sending failed, code %s.' % (res, )) return
def post(self): # Apply canonicalizations to the name. name = self.get_argument('name', '') name = unicodedata.normalize('NFKC', name) name = tornado.escape.squeeze(name.strip()) formerror = None if self.twsessionstatus == 'auth': formerror = 'You are already signed in!' elif (not name): formerror = 'You must enter your player name or email address.' if formerror: self.render('recover.html', formerror=formerror, init_name=name) return try: res = yield self.application.twsessionmgr.find_player_nopw( self, name) except MessageException as ex: formerror = str(ex) self.render('recover.html', formerror=formerror, init_name=name) return try: if not res: raise MessageException('There is no such player.') if res.get('guest', None): raise MessageException( 'The guest account password cannot be changed.') uid = res['_id'] email = res['email'] rec = yield motor.Op(self.application.mongodb.pwrecover.find_one, {'_id': uid}) if rec: raise MessageException( 'A recovery email has already been sent for this account. Please wait for it to arrive. If the message has been lost, you may try again in 24 hours.' ) # Invent a random key string, avoiding collision, even though # collision is unlikely. while True: key = self.application.twsessionmgr.random_bytes(16).decode() rec = yield motor.Op( self.application.mongodb.pwrecover.find_one, {'key': key}) if not rec: break rec = { '_id': uid, 'key': key, 'createtime': twcommon.misc.now(), } yield motor.Op(self.application.mongodb.pwrecover.insert, rec) self.application.twlog.warning('Player lost password: %s', email) # Send the email. We set this up as a subprocess call to # /bin/mail (or equivalent), using Tornado's async subprocess # wrapper. message = self.render_string( 'mail_recover.txt', hostname=self.application.twopts.hostname, key=key) mailer = tweblib.mailer.Mailer(self.application) yield mailer.send(email, 'Password change request', message) except MessageException as ex: formerror = str(ex) self.render('recover.html', formerror=formerror, init_name=name) return self.render('recover.html', mailsent=True)
def post(self): # If this flag is set, logins are restricted to administrators. locked = yield self.get_config_key('nologin') # If the "register" form was submitted, jump to the player- # registration page. if (self.get_argument('register', None)): self.redirect('/register') return # If the "guest login" form was submitted, create a guest session # and play. if (self.get_argument('guest', None)): try: if locked: raise MessageException( 'Sign-ins are not allowed at this time.') (res, email ) = yield self.application.twsessionmgr.create_session_guest( self) except MessageException as ex: formerror = str(ex) self.render('main.html', formerror=formerror) return self.application.twlog.info( 'Player signed in as guest: %s (session %s)', email, res) self.redirect('/play') return # Apply canonicalizations to the name and password. name = self.get_argument('name', '') name = unicodedata.normalize('NFKC', name) name = tornado.escape.squeeze(name.strip()) password = self.get_argument('password', '') password = unicodedata.normalize('NFKC', password) password = password.encode() # to UTF8 bytes formerror = None if (not name): formerror = 'You must enter your player name or email address.' elif (not password): formerror = 'You must enter your password.' if formerror: self.render('main.html', formerror=formerror, init_name=name) return try: res = yield self.application.twsessionmgr.find_player( self, name, password) except MessageException as ex: formerror = str(ex) self.render('main.html', formerror=formerror, init_name=name) return if not res: formerror = 'The name and password do not match.' self.render('main.html', formerror=formerror, init_name=name) return fieldname = name uid = res['_id'] email = res['email'] name = res['name'] if locked and not res.get('admin', None): formerror = 'Sign-ins are not allowed at this time.' self.render('main.html', formerror=formerror, init_name=name) return # Set a name cookie, for future form fill-in. This is whatever the # player entered in the form (name or email) self.set_cookie('tworld_name', tornado.escape.url_escape(fieldname), expires_days=30) res = yield self.application.twsessionmgr.create_session( self, uid, email, name) self.application.twlog.info('Player signed in: %s (session %s)', email, res) self.redirect('/play')