示例#1
0
    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
示例#2
0
    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
示例#3
0
    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
示例#4
0
 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']
示例#5
0
    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
示例#6
0
    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'])
示例#7
0
    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
示例#8
0
    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
示例#9
0
    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)
示例#10
0
    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')