Exemplo n.º 1
0
    def roster_update_users(self, e, sr):
        '''Update users' full names and invert hash

For all *users* we have information about:
- collect the shared roster groups they belong to
- set their full names if not yet defined
Return inverted hash'''
        groups = {}
        commands = []
        for user, desc in sr.items():
            if 'groups' in desc:
                for g in desc['groups']:
                    if g in groups:
                        groups[g].append(user)
                    else:
                        groups[g] = [user]
            if 'name' in desc:
                lhs, rhs = self.jidsplit(user)
                fnc = utf8('FNC:' + user)  # No unicode keys
                if fnc in self.ctx.shared_roster_db:
                    cached_name = unutf8(self.ctx.shared_roster_db[fnc])
                else:
                    cached_name = None
                self.ctx.shared_roster_db[fnc] = utf8(desc['name'])
                cmd = e.maybe_set_fn(lhs,
                                     rhs,
                                     desc['name'],
                                     cached_name=cached_name)
                if cmd is not None:
                    commands.append(cmd)
        return groups, commands
Exemplo n.º 2
0
class roster(roster_thread):
    def jidsplit(self, jid):
        '''Split jid into lhs@rhs'''
        (node, at, dom) = jid.partition('@')
        if at == '':
            return (node, self.domain)
        else:
            return (node, dom)

    def roster_cloud(self):
        '''Query roster JSON from cloud'''
        success, code, message, text = self.verbose_cloud_request({
            'operation': 'sharedroster',
            'username':  self.username,
            'domain':    self.authDomain
        })
        if success:
            if code is not None and code != requests.codes.ok:
                return code, None
            else:
                sr = None
                try:
                    sr = message['data']['sharedRoster']
                    return sr, text
                except Exception as e:
                    logging.warn('Weird response: ' + str(e))
                    return message, text
        else:
            return False, None

    def try_roster(self, async=True):
        '''Maybe update roster'''
        if (self.ctx.ejabberdctl_path is not None):
            try:
                response, text = self.roster_cloud()
                if response is not None and response != False:
                    texthash = hashlib.sha256(utf8(text)).hexdigest()
                    userhash = utf8('RH:' + self.username + ':' + self.domain)
                    # Response changed or first response for that user?
                    if not userhash in self.ctx.shared_roster_db or self.ctx.shared_roster_db[userhash] != texthash:
                        self.ctx.shared_roster_db[userhash] = texthash
                        t = threading.Thread(target=self.roster_background_thread,
                            args=[response])
                        t.start()
                        if not async:
                            t.join() # For automated testing only
                        else:
                            # Try to do most before the user is actually logged in.
                            # Thanks to improved caching, this should rarely be noticeable
                            # and reduce the 'full names only visible on second login'
                            # problem experienced especially in Gajim (maybe a race condition?)
                            t.join(1.0)
                        return True
            except Exception as err:
                (etype, value, tb) = sys.exc_info()
                traceback.print_exception(etype, value, tb)
                logging.warn('roster_groups thread: %s:\n%s'
                             % (str(err), ''.join(traceback.format_tb(tb))))
                return False
        return True
Exemplo n.º 3
0
    def roster_update_groups(self, e, groups):
        '''Update shared roster groups with ejabberdctl

For all the *groups* we have information about:
- create the group (idempotent)
- delete the users that we do not know about anymore
- add the users we know about (idempotent)'''
        cleanname = {}
        for g in groups:
            cleanname[g] = sanitize(g)
            key = utf8('RGC:%s:%s' % (cleanname[g], self.domain))
            if key in self.ctx.shared_roster_db:
                previous_users = self.ctx.shared_roster_db[key].split('\t')
            else:
                e.execute([
                    'srg_create', cleanname[g], self.domain, cleanname[g],
                    cleanname[g], cleanname[g]
                ])
                # Fill cache (again)
                previous_users = e.members(cleanname[g], self.domain)
            new_users = {}
            for u in groups[g]:
                (lhs, rhs) = self.jidsplit(u)
                fulljid = '%s@%s' % (lhs, rhs)
                new_users[fulljid] = True
                if not fulljid in previous_users:
                    e.execute(
                        ['srg_user_add', lhs, rhs, cleanname[g], self.domain])
            for p in previous_users:
                (lhs, rhs) = self.jidsplit(p)
                if p not in new_users:
                    e.execute(
                        ['srg_user_del', lhs, rhs, cleanname[g], self.domain])
            self.ctx.shared_roster_db[key] = '\t'.join(sorted(
                new_users.keys()))

        # For all the groups the login user was previously a member of:
        # - delete her from the shared roster group if no longer a member
        key = utf8('LIG:%s@%s' % (self.username, self.domain))
        if key in self.ctx.shared_roster_db and self.ctx.shared_roster_db[
                key] != '':
            # Was previously there as well, need to be removed from one?
            previous = self.ctx.shared_roster_db[key].split('\t')
            for p in previous:
                if p not in list(cleanname.values()):
                    e.execute([
                        'srg_user_del', self.username, self.domain, p,
                        self.domain
                    ])
            # Only update when necessary
            if not cleanname:
                del self.ctx.shared_roster_db[key]
            else:
                new = '\t'.join(sorted(cleanname.values()))
                if previous != new:
                    self.ctx.shared_roster_db[key] = new
        else:  # New, always set
            if cleanname:
                self.ctx.shared_roster_db[key] = '\t'.join(
                    sorted(cleanname.values()))
Exemplo n.º 4
0
 def checkpw(self, pwhash):
     '''Compare self.password with pwhash.
     
     Try to be resistant to timing attacks and use `checkpw` if available.'''
     pw = utf8(self.password)
     pwhash = utf8(pwhash)
     if 'checkpw' in dir(bcrypt):
         return bcrypt.checkpw(pw, pwhash)
     else:
         ret = bcrypt.hashpw(pw, pwhash)
         return ret == pwhash
Exemplo n.º 5
0
 def per_domain(self, dom):
     bdom = utf8(dom)
     if bdom in self.domain_db:
         try:
             # Already 4-value database format? Great!
             secret, url, authDomain, extra = self.domain_db[bdom].split('\t', 3)
         except ValueError:
             # No, fall back to 3-value format (and update DB)
             secret, url, extra = self.domain_db[bdom].split('\t', 2)
             authDomain = dom
             self.domain_db[dom] = '\t'.join((secret, url, authDomain, extra))
         return utf8(secret), url, authDomain
     else:
         return utf8(self.default_secret), self.default_url, dom
Exemplo n.º 6
0
 def auth_update_cache(self):
     if self.ctx.db.cache_storage == 'none':
         return False
     jid = self.username + '@' + self.domain
     now = self.now
     try:
         if self.ctx.db.cache_storage == 'memory':
             rounds = self.ctx.bcrypt_rounds[1]
         else:
             rounds = self.ctx.bcrypt_rounds[0]
         salt = bcrypt.gensalt(rounds=rounds)
     except TypeError:
         # Old versions of bcrypt() apparently do not support the rounds option
         salt = bcrypt.gensalt()
     pwhash = unutf8(bcrypt.hashpw(utf8(self.password), salt))
     # Upsert in SQLite is too new to rely on:
     # https://www.sqlite.org/draft/lang_UPSERT.html
     #
     # INSERT OR REPLACE cannot be used, as it will inherit
     # the DEFAULT values instead of the existing values.
     self.ctx.db.cache.begin()
     self.ctx.db.cache.execute(
             '''INSERT OR IGNORE INTO authcache (jid, firstauth)
             VALUES (?, ?)''',
             (jid, now))
     self.ctx.db.cache.execute(
             '''UPDATE authcache
             SET pwhash = ?, remoteauth = ?, anyauth = ?
             WHERE jid = ?''', (pwhash, now, now, jid))
     self.ctx.db.cache.commit()
Exemplo n.º 7
0
    def auth_token(self):
        try:
            token = b64decode(
                self.password.translate(usersafe_encoding) + '=======')
        except:
            logging.debug('Could not decode token (maybe not a token?)')
            return False

        jid = self.username + '@' + self.domain

        if len(token) != 23:
            logging.debug('Token is too short: %d != 23 (maybe not a token?)' %
                          len(token))
            return False

        (version, mac, header) = unpack('> B 16s 6s', token)
        if version != 0:
            logging.debug('Wrong token version (maybe not a token?)')
            return False

        (secretID, expiry) = unpack('> H I', header)
        if expiry < self.now:
            logging.debug('Token has expired')
            return False

        challenge = pack('> B 6s %ds' % len(jid), version, header, utf8(jid))
        response = hmac.new(self.secret, challenge, hashlib.sha256).digest()

        return hmac.compare_digest(mac, response[:16])
Exemplo n.º 8
0
    def auth_token(self):
        try:
            token = b64decode(self.password.translate(usersafe_encoding) + '=======')
        except:
            logging.debug('Not a token (not base64)')
            return False

        jid = self.username + '@' + self.domain

        if len(token) != 23:
            logging.debug('Not a token (len: %d != 23)' % len(token))
            return False

        (version, mac, header) = unpack('> B 16s 6s', token)
        if version != 0:
            logging.debug('Not a token (version: %d != 0)' % version)
            return False;

        (secretID, expiry) = unpack('> H I', header)
        expiry = datetime.utcfromtimestamp(expiry)
        if expiry < self.now:
            logging.debug('Token has expired')
            return False

        challenge = pack('> B 6s %ds' % len(jid), version, header, utf8(jid))
        response = hmac.new(self.secret, challenge, hashlib.sha256).digest()
        if hmac.compare_digest(mac, response[:16]):
            return True
        else:
            logging.warning('Token for %s has invalid signature (possible attack attempt!)' % jid)
            return False
Exemplo n.º 9
0
def perform(args):
    domain_db = bsddb3.hashopen(args.domain_db, 'c', 0o600)
    if args.get:
        print(unutf8(domain_db[utf8(args.get)]))
    elif args.put:
        domain_db[utf8(args.put[0])] = args.put[1]
    elif args.delete:
        del domain_db[utf8(args.delete)]
    elif args.unload:
        for k in list(domain_db.keys()):
            print('%s\t%s' % (unutf8(k), unutf8(domain_db[k])))
        # Should work according to documentation, but doesn't
        # for k, v in DOMAIN_DB.iteritems():
        #     print k, '\t', v
    elif args.load:
        for line in sys.stdin:
            k, v = line.rstrip('\r\n').split('\t', 1)
            domain_db[utf8(k)] = v
    domain_db.close()
Exemplo n.º 10
0
 def test_05_unload(self):
     expected = [b'example.net', b'example.ch']
     self.stub_stdout(ioclass=io.StringIO)
     ns = self.mkns(unload=True)
     perform(ns)
     v = sys.stdout.getvalue()
     for line in v.split('\n'):
         if line != '':
             (k, delim, v) = line.partition('\t')
             expected.remove(utf8(k))
     assertEqual(expected, [])
Exemplo n.º 11
0
 def verbose_cloud_request(self, data):
     '''Perform a signed cloud request on data with detailed result.
     
     Return tuple:
     - (True, None, json, body): Remote side answered with HTTP 200 and JSON body
     - (False, 200, None, None): Remote side answered with HTTP 200, but no JSON
     - (False, int, json, body): Remote side answered != 200, with JSON body
     - (False, int, None, None): Remote side answered != 200, without JSON
     - (False, None, err, None): Connection problem, described in err
     '''
     # logging.debug("Sending %s to %s" % (data, url))
     payload = utf8(urllib.parse.urlencode(data))
     signature = hmac.new(self.secret, msg=payload,
                          digestmod=hashlib.sha1).hexdigest()
     headers = {
         'X-JSXC-SIGNATURE': 'sha1=' + signature,
         'content-type': 'application/x-www-form-urlencoded'
     }
     try:
         r = self.ctx.session.post(self.url,
                                   data=payload,
                                   headers=headers,
                                   allow_redirects=False,
                                   timeout=self.ctx.timeout)
     except requests.exceptions.HTTPError as err:
         logging.warn(err)
         return False, None, err, None
     except requests.exceptions.RequestException as err:
         try:
             logging.warn(
                 'An error occured during the request to %s for domain %s: %s'
                 % (self.url, data['domain'], err))
         except TypeError as err:
             logging.warn(
                 'An unknown error occured during the request to %s, probably an SSL error. Try updating your "requests" and "urllib" libraries.'
                 % url)
         return False, None, err, None
     if r.status_code != requests.codes.ok:
         try:
             return False, r.status_code, r.json(), r.text
         except ValueError:  # Not a valid JSON response
             return False, r.status_code, None, None
     try:
         # Return True only for HTTP 200 with JSON body, False for everything else
         return True, None, r.json(), r.text
     except ValueError:  # Not a valid JSON response
         return False, r.status_code, None, None
Exemplo n.º 12
0
 def try_roster(self, async_=True):
     '''Maybe update roster'''
     if (self.ctx.ejabberdctl_path is not None):
         try:
             response, text = self.roster_cloud()
             if response is not None and response != False:
                 jid = '@'.join((self.username, self.domain))
                 texthash = hashlib.sha256(utf8(text)).hexdigest()
                 # Response changed or first response for that user?
                 cache_valid = False
                 for row in self.ctx.db.conn.execute(
                         'SELECT responsehash FROM rosterinfo where jid=?',
                     (jid, )):
                     if row['responsehash'] == texthash:
                         cache_valid = True
                 if not cache_valid:
                     self.ctx.db.conn.begin()
                     self.ctx.db.conn.execute(
                         '''INSERT OR IGNORE
                             INTO rosterinfo (jid)
                             VALUES (?)''', (jid, ))
                     self.ctx.db.conn.execute(
                         '''UPDATE rosterinfo
                             SET responsehash = ?, last_update = ?
                             WHERE jid = ?''',
                         (texthash, datetime.utcnow(), jid))
                     self.ctx.db.conn.commit()
                     t = threading.Thread(
                         target=self.roster_background_thread,
                         args=(response, ))
                     t.start()
                     if not async_:
                         t.join()  # For automated testing only
                     else:
                         # Try to do most before the user is actually logged in.
                         # Thanks to improved caching, this should rarely be noticeable
                         # and reduce the 'full names only visible on second login'
                         # problem experienced especially in Gajim (maybe a race condition?)
                         t.join(1.0)
                     return True
         except Exception as err:
             (etype, value, tb) = sys.exc_info()
             traceback.print_exception(etype, value, tb)
             logging.warn('roster_groups thread: %s:\n%s' %
                          (str(err), ''.join(traceback.format_tb(tb))))
             return False
     return True
Exemplo n.º 13
0
 def auth_update_cache(self):
     if '' in self.ctx.cache_db:  # Cache disabled?
         return
     key = self.username + ':' + self.domain
     now = self.now  # For tests
     snow = str(now)
     try:
         salt = bcrypt.gensalt(rounds=self.ctx.bcrypt_rounds)
     except TypeError:
         # Old versions of bcrypt() apparently do not support the rounds option
         salt = bcrypt.gensalt()
     pwhash = unutf8(bcrypt.hashpw(utf8(self.password), salt))
     if key in self.ctx.cache_db:
         (ignored, ts1, tsv, tsa,
          rest) = self.ctx.cache_db[key].split("\t", 4)
         self.ctx.cache_db[key] = "\t".join((pwhash, ts1, snow, snow, rest))
     else:
         self.ctx.cache_db[key] = "\t".join((pwhash, snow, snow, snow, ''))
     self.try_db_sync()
Exemplo n.º 14
0
class roster(roster_thread):
    def jidsplit(self, jid):
        '''Split jid into lhs@rhs'''
        (node, at, dom) = jid.partition('@')
        if at == '':
            return (node, self.domain)
        else:
            return (node, dom)

    def roster_cloud(self):
        '''Query roster JSON from cloud'''
        success, code, message, text = self.verbose_cloud_request({
            'operation':
            'sharedroster',
            'username':
            self.username,
            'domain':
            self.authDomain
        })
        if success:
            if code is not None and code != requests.codes.ok:
                return code, None
            else:
                sr = None
                try:
                    sr = message['data']['sharedRoster']
                    return sr, text
                except Exception as e:
                    logging.warn('Weird response: ' + str(e))
                    return message, text
        else:
            return False, None

    def try_roster(self, async=True):
        '''Maybe update roster'''
        if (self.ctx.ejabberdctl_path is not None):
            try:
                response, text = self.roster_cloud()
                if response is not None and response != False:
                    jid = '@'.join((self.username, self.domain))
                    texthash = hashlib.sha256(utf8(text)).hexdigest()
                    # Response changed or first response for that user?
                    cache_valid = False
                    for row in self.ctx.db.conn.execute(
                            'SELECT responsehash FROM rosterinfo where jid=?',
                        (jid, )):
                        if row['responsehash'] == texthash:
                            cache_valid = True
                    if not cache_valid:
                        self.ctx.db.conn.begin()
                        self.ctx.db.conn.execute(
                            '''INSERT OR IGNORE
                                INTO rosterinfo (jid)
                                VALUES (?)''', (jid, ))
                        self.ctx.db.conn.execute(
                            '''UPDATE rosterinfo
                                SET responsehash = ?, last_update = ?
                                WHERE jid = ?''',
                            (texthash, datetime.utcnow(), jid))
                        self.ctx.db.conn.commit()
                        t = threading.Thread(
                            target=self.roster_background_thread,
                            args=(response, ))
                        t.start()
                        if not async:
                            t.join()  # For automated testing only
                        else:
                            # Try to do most before the user is actually logged in.
                            # Thanks to improved caching, this should rarely be noticeable
                            # and reduce the 'full names only visible on second login'
                            # problem experienced especially in Gajim (maybe a race condition?)
                            t.join(1.0)
                        return True
            except Exception as err:
                (etype, value, tb) = sys.exc_info()
                traceback.print_exception(etype, value, tb)
                logging.warn('roster_groups thread: %s:\n%s' %
                             (str(err), ''.join(traceback.format_tb(tb))))
                return False
        return True
Exemplo n.º 15
0
def test_utf8_ascii():
    assertEqual(b'hallo', utf8(u'hallo'))
Exemplo n.º 16
0
def test_utf8_valid():
    assertEqual(b'Hall\xc3\xb6chen', utf8(u'Hallöchen'))
Exemplo n.º 17
0
 def per_domain(self, dom):
     for row in self.db.conn.execute('SELECT authsecret, authurl, authdomain FROM domains WHERE xmppdomain = ?', (dom,)):
         return utf8(row[0]), row[1], row[2]
     return utf8(self.default_secret), self.default_url, dom