def getMxid(self, medium: str, normalised_address: str) -> Optional[str]:
        """
        Retrieves the MXID associated with a 3PID. Please note that
        emails need to be casefolded before calling this function.

        :param medium: The medium of the 3PID.
        :param normalised_address: The address of the 3PID.

        :return: The associated MXID, or None if no MXID is associated with this 3PID.
        """

        cur = self.sydent.db.cursor()
        res = cur.execute(
            "select mxid from global_threepid_associations where "
            "medium = ? and lower(address) = lower(?) and notBefore < ? and notAfter > ? "
            "order by ts desc limit 1",
            (medium, normalised_address, time_msec(), time_msec()),
        )

        row: Tuple[Optional[str]] = res.fetchone()

        if not row:
            return None

        return row[0]
    def signedAssociationStringForThreepid(self, medium: str,
                                           address: str) -> Optional[str]:
        """
        Retrieve the JSON for the signed association matching the provided 3PID,
        if one exists.

        :param medium: The medium of the 3PID.
        :param address: The address of the 3PID.

        :return: The signed association, or None if no association was found for this
            3PID.
        """

        cur = self.sydent.db.cursor()
        # We treat address as case-insensitive because that's true for all the
        # threepids we have currently (we treat the local part of email addresses as
        # case insensitive which is technically incorrect). If we someday get a
        # case-sensitive threepid, this can change.
        res = cur.execute(
            "select sgAssoc from global_threepid_associations where "
            "medium = ? and lower(address) = lower(?) and notBefore < ? and notAfter > ? "
            "order by ts desc limit 1",
            (medium, address, time_msec(), time_msec()),
        )

        row: Optional[Tuple[str]] = res.fetchone()

        if not row:
            return None

        sgAssocStr = row[0]

        return sgAssocStr
Example #3
0
    def getOrCreateTokenSession(self, medium, address, clientSecret):
        cur = self.sydent.db.cursor()

        cur.execute("select s.id, s.medium, s.address, s.clientSecret, s.validated, s.mtime, "
                    "t.token, t.sendAttemptNumber from threepid_validation_sessions s,threepid_token_auths t "
                    "where s.medium = ? and s.address = ? and s.clientSecret = ? and t.validationSession = s.id",
                    (medium, address, clientSecret))
        row = cur.fetchone()

        if row:
            s = ValidationSession(row[0], row[1], row[2], row[3], row[4], row[5])
            s.token = row[6]
            s.sendAttemptNumber = row[7]
            return s

        sid = self.addValSession(medium, address, clientSecret, time_msec(), commit=False)

        tokenString = sydent.util.tokenutils.generateNumericTokenOfLength(
            int(self.sydent.cfg.get('email', 'token.length')))

        cur.execute("insert into threepid_token_auths (validationSession, token, sendAttemptNumber) values (?, ?, ?)",
                    (sid, tokenString, -1))
        self.sydent.db.commit()

        s = ValidationSession(sid, medium, address, clientSecret, False, time_msec())
        s.token = tokenString
        s.sendAttemptNumber = -1
        return s
Example #4
0
    def getMxid(self, medium, address):
        """
        Retrieves the MXID associated with a 3PID.

        :param medium: The medium of the 3PID.
        :type medium: unicode
        :param address: The address of the 3PID.
        :type address: unicode

        :return: The associated MXID, or None if no MXID is associated with this 3PID.
        :rtype: unicode or None
        """
        cur = self.sydent.db.cursor()
        res = cur.execute(
            "select mxid from global_threepid_associations where "
            "medium = ? and lower(address) = lower(?) and notBefore < ? and notAfter > ? "
            "order by ts desc limit 1",
            (medium, address, time_msec(), time_msec()))

        row = res.fetchone()

        if not row:
            return None

        return row[0]
Example #5
0
    def getOrCreateTokenSession(self, medium, address, clientSecret):
        cur = self.sydent.db.cursor()

        cur.execute(
            "select s.id, s.medium, s.address, s.clientSecret, s.validated, s.mtime, "
            "t.token, t.sendAttemptNumber from threepid_validation_sessions s,threepid_token_auths t "
            "where s.medium = ? and s.address = ? and s.clientSecret = ? and t.validationSession = s.id",
            (medium, address, clientSecret))
        row = cur.fetchone()

        if row:
            s = ValidationSession(row[0], row[1], row[2], row[3], row[4],
                                  row[5])
            s.token = row[6]
            s.sendAttemptNumber = row[7]
            return s

        sid = self.addValSession(medium,
                                 address,
                                 clientSecret,
                                 time_msec(),
                                 commit=False)

        tokenString = sydent.util.tokenutils.generateTokenForMedium(medium)

        cur.execute(
            "insert into threepid_token_auths (validationSession, token, sendAttemptNumber) values (?, ?, ?)",
            (sid, tokenString, -1))
        self.sydent.db.commit()

        s = ValidationSession(sid, medium, address, clientSecret, False,
                              time_msec())
        s.token = tokenString
        s.sendAttemptNumber = -1
        return s
    def retrieveMxidsForHashes(self, addresses: List[str]) -> Dict[str, str]:
        """Returns a mapping from hash: mxid from a list of given lookup_hash values

        :param addresses: An array of lookup_hash values to check against the db

        :returns a dictionary of lookup_hash values to mxids of all discovered matches
        """
        cur = self.sydent.db.cursor()

        cur.execute("CREATE TEMPORARY TABLE tmp_retrieve_mxids_for_hashes "
                    "(lookup_hash VARCHAR)")
        cur.execute(
            "CREATE INDEX tmp_retrieve_mxids_for_hashes_lookup_hash ON "
            "tmp_retrieve_mxids_for_hashes(lookup_hash)")

        results = {}
        try:
            # Convert list of addresses to list of tuples of addresses
            tuplized_addresses = [(x, ) for x in addresses]

            inserted_cap = 0
            while inserted_cap < len(tuplized_addresses):
                cur.executemany(
                    "INSERT INTO tmp_retrieve_mxids_for_hashes(lookup_hash) "
                    "VALUES (?)",
                    tuplized_addresses[inserted_cap:inserted_cap + 500],
                )
                inserted_cap += 500

            res = cur.execute(
                # 'notBefore' is the time the association starts being valid, 'notAfter' the the time at which
                # it ceases to be valid, so the ts must be greater than 'notBefore' and less than 'notAfter'.
                "SELECT gta.lookup_hash, gta.mxid FROM global_threepid_associations gta "
                "JOIN tmp_retrieve_mxids_for_hashes "
                "ON gta.lookup_hash = tmp_retrieve_mxids_for_hashes.lookup_hash "
                "WHERE gta.notBefore < ? AND gta.notAfter > ? "
                "ORDER BY gta.lookup_hash, gta.mxid, gta.ts",
                (time_msec(), time_msec()),
            )

            # Place the results from the query into a dictionary
            # Results are sorted from oldest to newest, so if there are multiple mxid's for
            # the same lookup hash, only the newest mapping will be returned

            # Type safety: lookup_hash is a nullable string in
            # global_threepid_associations. But it must be equal to a lookup_hash
            # in the temporary table thanks to the join condition.
            # The temporary table gets hashes from the `addresses` argument,
            # which is a list of (non-None) strings.
            # So lookup_hash really is a str.
            lookup_hash: str
            mxid: str
            for lookup_hash, mxid in res.fetchall():
                results[lookup_hash] = mxid

        finally:
            cur.execute("DROP TABLE tmp_retrieve_mxids_for_hashes")

        return results
    def getMxids(
            self,
            threepid_tuples: List[Tuple[str,
                                        str]]) -> List[Tuple[str, str, str]]:
        """Given a list of threepid_tuples, return the same list but with
        mxids appended to each tuple for which a match was found in the
        database for. Output is ordered by medium, address, timestamp DESC

        :param threepid_tuples: List containing (medium, address) tuples

        :return: a list of (medium, address, mxid) tuples
        """
        cur = self.sydent.db.cursor()

        cur.execute(
            "CREATE TEMPORARY TABLE tmp_getmxids (medium VARCHAR(16), address VARCHAR(256))"
        )
        cur.execute(
            "CREATE INDEX tmp_getmxids_medium_lower_address ON tmp_getmxids (medium, lower(address))"
        )

        try:
            inserted_cap = 0
            while inserted_cap < len(threepid_tuples):
                cur.executemany(
                    "INSERT INTO tmp_getmxids (medium, address) VALUES (?, ?)",
                    threepid_tuples[inserted_cap:inserted_cap + 500],
                )
                inserted_cap += 500

            res = cur.execute(
                # 'notBefore' is the time the association starts being valid, 'notAfter' the the time at which
                # it ceases to be valid, so the ts must be greater than 'notBefore' and less than 'notAfter'.
                "SELECT gte.medium, gte.address, gte.ts, gte.mxid FROM global_threepid_associations gte "
                "JOIN tmp_getmxids ON gte.medium = tmp_getmxids.medium AND lower(gte.address) = lower(tmp_getmxids.address) "
                "WHERE gte.notBefore < ? AND gte.notAfter > ? "
                "ORDER BY gte.medium, gte.address, gte.ts DESC",
                (time_msec(), time_msec()),
            )

            results = []
            current = None
            row: Tuple[str, str, int, str]
            for row in res.fetchall():
                # only use the most recent entry for each
                # threepid (they're sorted by ts)
                if (row[0], row[1]) == current:
                    continue
                current = (row[0], row[1])
                results.append((row[0], row[1], row[3]))

        finally:
            cur.execute("DROP TABLE tmp_getmxids")

        return results
Example #8
0
    def getOrCreateTokenSession(
            self, medium: str, address: str,
            clientSecret: str) -> Tuple[ValidationSession, TokenInfo]:
        """
        Retrieves the validation session for a given medium, address and client secret,
        or creates one if none was found.

        :param medium: The medium to use when looking up or creating the session.
        :param address: The address to use when looking up or creating the session.
        :param clientSecret: The client secret to use when looking up or creating the
            session.

        :return: The session that was retrieved or created.
        """
        cur = self.sydent.db.cursor()

        cur.execute(
            "select s.id, s.medium, s.address, s.clientSecret, s.validated, s.mtime, "
            "t.token, t.sendAttemptNumber from threepid_validation_sessions s,threepid_token_auths t "
            "where s.medium = ? and s.address = ? and s.clientSecret = ? and t.validationSession = s.id",
            (medium, address, clientSecret),
        )
        row: Optional[Tuple[int, str, str, str, Optional[int], int, str,
                            int]] = cur.fetchone()

        if row:
            session = ValidationSession(row[0], row[1], row[2], row[3],
                                        bool(row[4]), row[5])
            token_info = TokenInfo(row[6], row[7])
            return session, token_info

        sid = self.addValSession(medium,
                                 address,
                                 clientSecret,
                                 time_msec(),
                                 commit=False)

        tokenString = sydent.util.tokenutils.generateTokenForMedium(medium)

        cur.execute(
            "insert into threepid_token_auths (validationSession, token, sendAttemptNumber) values (?, ?, ?)",
            (sid, tokenString, -1),
        )
        self.sydent.db.commit()

        session = ValidationSession(
            sid,
            medium,
            address,
            clientSecret,
            False,
            time_msec(),
        )
        token_info = TokenInfo(tokenString, -1)
        return session, token_info
Example #9
0
    def getMxid(self, medium, address):
        cur = self.sydent.db.cursor()
        res = cur.execute("select mxid from global_threepid_associations where "
                    "medium = ? and lower(address) = lower(?) and notBefore < ? and notAfter > ? "
                    "order by ts desc limit 1",
                    (medium, address, time_msec(), time_msec()))

        row = res.fetchone()

        if not row:
            return None

        return row[0]
Example #10
0
    def retrieveMxidsForHashes(self, addresses):
        """Returns a mapping from hash: mxid from a list of given lookup_hash values

        :param addresses: An array of lookup_hash values to check against the db
        :type addresses: list[unicode]

        :returns a dictionary of lookup_hash values to mxids of all discovered matches
        :rtype: dict[unicode, unicode]
        """
        cur = self.sydent.db.cursor()

        cur.execute("CREATE TEMPORARY TABLE tmp_retrieve_mxids_for_hashes "
                    "(lookup_hash VARCHAR)")
        cur.execute(
            "CREATE INDEX tmp_retrieve_mxids_for_hashes_lookup_hash ON "
            "tmp_retrieve_mxids_for_hashes(lookup_hash)")

        results = {}
        try:
            # Convert list of addresses to list of tuples of addresses
            addresses = [(x, ) for x in addresses]

            inserted_cap = 0
            while inserted_cap < len(addresses):
                cur.executemany(
                    "INSERT INTO tmp_retrieve_mxids_for_hashes(lookup_hash) "
                    "VALUES (?)", addresses[inserted_cap:inserted_cap + 500])
                inserted_cap += 500

            res = cur.execute(
                # 'notBefore' is the time the association starts being valid, 'notAfter' the the time at which
                # it ceases to be valid, so the ts must be greater than 'notBefore' and less than 'notAfter'.
                "SELECT gta.lookup_hash, gta.mxid FROM global_threepid_associations gta "
                "JOIN tmp_retrieve_mxids_for_hashes "
                "ON gta.lookup_hash = tmp_retrieve_mxids_for_hashes.lookup_hash "
                "WHERE gta.notBefore < ? AND gta.notAfter > ? "
                "ORDER BY gta.lookup_hash, gta.mxid, gta.ts",
                (time_msec(), time_msec()))

            # Place the results from the query into a dictionary
            # Results are sorted from oldest to newest, so if there are multiple mxid's for
            # the same lookup hash, only the newest mapping will be returned
            for lookup_hash, mxid in res.fetchall():
                results[lookup_hash] = mxid

        finally:
            cur.execute("DROP TABLE tmp_retrieve_mxids_for_hashes")

        return results
Example #11
0
    def signedAssociationStringForThreepid(self, medium, address):
        cur = self.sydent.db.cursor()
        res = cur.execute("select sgAssoc from global_threepid_associations where "
                    "medium = ? and address = ? and notBefore < ? and notAfter > ? "
                    "order by ts desc limit 1",
                    (medium, address, time_msec(), time_msec()))

        row = res.fetchone()

        if not row:
            return None

        sgAssocBytes = row[0]

        return sgAssocBytes
Example #12
0
def validateSessionWithToken(sydent, sid, clientSecret, token):
    """
    Attempt to validate a session, identified by the sid, using
    the token from out-of-band. The client secret is given to
    prevent attempts to guess the token for a sid.
    If the session was sucessfully validated, return a dict
    with 'success': True that can be sent to the client,
    otherwise return False.
    """
    valSessionStore = ThreePidValSessionStore(sydent)
    s = valSessionStore.getTokenSessionById(sid)
    if not s:
        logger.info("Session ID %s not found", (sid))
        return False

    if not clientSecret == s.clientSecret:
        logger.info("Incorrect client secret", (sid))
        raise IncorrectClientSecretException()

    if s.mtime + ValidationSession.THREEPID_SESSION_VALIDATION_TIMEOUT_MS < time_msec():
        logger.info("Session expired")
        raise SessionExpiredException()

    # TODO once we can validate the token oob
    #if tokenObj.validated and clientSecret == tokenObj.clientSecret:
    #    return True

    if s.token == token:
        logger.info("Setting session %s as validated", (s.id))
        valSessionStore.setValidated(s.id, True)

        return {'success': True}
    else:
        logger.info("Incorrect token submitted")
        return False
Example #13
0
def sendEmail(sydent, templateName, mailTo, substitutions):
        mailFrom = sydent.cfg.get('email', 'email.from')
        mailTemplateFile = sydent.cfg.get('email', templateName)

        myHostname = os.uname()[1]
        midRandom = "".join([random.choice(string.ascii_letters) for _ in range(16)])
        messageid = "<%d%s@%s>" % (time_msec(), midRandom, myHostname)

        allSubstitutions = {}
        allSubstitutions.update(substitutions)
        allSubstitutions.update({
            'messageid': messageid,
            'date': email.utils.formatdate(localtime=False),
            'to': mailTo,
            'from': mailFrom,
        })
        mailString = open(mailTemplateFile).read() % allSubstitutions
        rawFrom = email.utils.parseaddr(mailFrom)[1]
        rawTo = email.utils.parseaddr(mailTo)[1]
        if rawFrom == '' or rawTo == '':
            logger.info("Couldn't parse from / to address %s / %s", mailFrom, mailTo)
            raise EmailAddressException()
        mailServer = sydent.cfg.get('email', 'email.smtphost')
        logger.info("Sending mail to %s with mail server: %s" % (mailTo, mailServer,))
        try:
            smtp = smtplib.SMTP(mailServer)
            smtp.sendmail(rawFrom, rawTo, mailString)
            smtp.quit()
        except Exception as origException:
            twisted.python.log.err()
            ese = EmailSendException()
            ese.cause = origException
            raise ese
Example #14
0
    def validateSessionWithToken(self, sid, clientSecret, token):
        valSessionStore = ThreePidValSessionStore(self.sydent)
        s = valSessionStore.getTokenSessionById(sid)
        if not s:
            logger.info("Session ID %s not found", (sid))
            return False

        if not clientSecret == s.clientSecret:
            logger.info("Incorrect client secret", (sid))
            raise IncorrectClientSecretException()

        if s.mtime + ValidationSession.THREEPID_SESSION_VALIDATION_TIMEOUT_MS < time_msec(
        ):
            logger.info("Session expired")
            raise SessionExpiredException()

        # TODO once we can validate the token oob
        #if tokenObj.validated and clientSecret == tokenObj.clientSecret:
        #    return True

        if s.token == token:
            logger.info("Setting session %s as validated", (s.id))
            valSessionStore.setValidated(s.id, True)

            return {'success': True}
        else:
            logger.info("Incorrect token submitted")
            return False
Example #15
0
    def removeAssociation(self, threepid, mxid):
        cur = self.sydent.db.cursor()

        # check to see if we have any matching associations first.
        # We use a REPLACE INTO because we need the resulting row to have
        # a new ID (such that we know it's a new change that needs to be
        # replicated) so there's no need to insert a deletion row if there's
        # nothing to delete.
        cur.execute(
            "SELECT COUNT(*) FROM local_threepid_associations "
            "WHERE medium = ? AND address = ? AND mxid = ?",
            (threepid['medium'], threepid['address'], mxid)
        )
        row = cur.fetchone()
        if row[0] > 0:
            ts = time_msec()
            cur.execute(
                "REPLACE INTO local_threepid_associations "
                "('medium', 'address', 'mxid', 'ts', 'notBefore', 'notAfter') "
                " values (?, ?, NULL, ?, null, null)",
                (threepid['medium'], threepid['address'], ts),
            )
            logger.info(
                "Deleting local assoc for %s/%s/%s replaced %d rows",
                threepid['medium'], threepid['address'], mxid, cur.rowcount,
            )
            self.sydent.db.commit()
        else:
            logger.info(
                "No local assoc found for %s/%s/%s",
                threepid['medium'], threepid['address'], mxid,
            )
            raise ValueError("No match found between provided mxid and threepid")
Example #16
0
    def getValidatedSession(self, sid: int,
                            client_secret: str) -> ValidationSession:
        """
        Retrieve a validated and still-valid session whose client secret matches the
        one passed in.

        :param sid: The ID of the session to retrieve.
        :param client_secret: A client secret to check against the one retrieved from
            the database.

        :return: The retrieved session.

        :raise InvalidSessionIdException: No session could be found with this ID.
        :raise IncorrectClientSecretException: The session's client secret doesn't
            match the one passed in.
        :raise SessionExpiredException: The session exists but has expired.
        :raise SessionNotValidatedException: The session exists but hasn't been
            validated yet.
        """
        s = self.getSessionById(sid)

        if not s:
            raise InvalidSessionIdException()

        if not s.client_secret == client_secret:
            raise IncorrectClientSecretException()

        if s.mtime + THREEPID_SESSION_VALID_LIFETIME_MS < time_msec():
            raise SessionExpiredException()

        if not s.validated:
            raise SessionNotValidatedException()

        return s
Example #17
0
    def validateSessionWithToken(self, sid, clientSecret, token):
        valSessionStore = ThreePidValSessionStore(self.sydent)
        s = valSessionStore.getTokenSessionById(sid)
        if not s:
            logger.info("Session ID %s not found", (sid))
            return False

        if not clientSecret == s.clientSecret:
            logger.info("Incorrect client secret", (sid))
            raise IncorrectClientSecretException()

        if s.mtime + ValidationSession.THREEPID_SESSION_VALIDATION_TIMEOUT < time_msec():
            logger.info("Session expired")
            raise SessionExpiredException()

        # TODO once we can validate the token oob
        #if tokenObj.validated and clientSecret == tokenObj.clientSecret:
        #    return True

        if s.token == token:
            logger.info("Setting session %s as validated", (s.id))
            valSessionStore.setValidated(s.id, True)

            return {'success': True}
        else:
            logger.info("Incorrect token submitted")
            return False
Example #18
0
    def requestToken(self, phoneNumber, clientSecret, sendAttempt, nextLink):
        valSessionStore = ThreePidValSessionStore(self.sydent)

        msisdn = phonenumbers.format_number(
            phoneNumber, phonenumbers.PhoneNumberFormat.E164
        )[1:]

        valSession = valSessionStore.getOrCreateTokenSession(
            medium='msisdn', address=msisdn, clientSecret=clientSecret
        )

        valSessionStore.setMtime(valSession.id, time_msec())

        if int(valSession.sendAttemptNumber) >= int(sendAttempt):
            logger.info("Not texting code because current send attempt (%d) is not less than given send attempt (%s)", int(sendAttempt), int(valSession.sendAttemptNumber))
            return valSession.id

        smsBodyTemplate = self.sydent.cfg.get('sms', 'bodyTemplate')
        originator = self.getOriginator(phoneNumber)

        logger.info(
            "Attempting to text code %s to %s (country %d) with originator %s",
            valSession.token, msisdn, phoneNumber.country_code, originator
        )

        smsBody = smsBodyTemplate.format(token=valSession.token)

        self.omSms.sendTextSMS(smsBody, msisdn, originator)

        valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt)

        return valSession.id
    def signedAssociationStringForThreepid(self, medium, address):
        cur = self.sydent.db.cursor()
        res = cur.execute(
            "select sgAssoc from global_threepid_associations where "
            "medium = ? and address = ? and notBefore < ? and notAfter > ? "
            "order by ts desc limit 1",
            (medium, address, time_msec(), time_msec()))

        row = res.fetchone()

        if not row:
            return None

        sgAssocBytes = row[0]

        return sgAssocBytes
Example #20
0
    def requestToken(self, emailAddress, clientSecret, sendAttempt, nextLink, ipaddress=None):
        valSessionStore = ThreePidValSessionStore(self.sydent)

        valSession = valSessionStore.getOrCreateTokenSession(medium='email', address=emailAddress,
                                                             clientSecret=clientSecret)

        valSessionStore.setMtime(valSession.id, time_msec())

        if int(valSession.sendAttemptNumber) >= int(sendAttempt):
            logger.info("Not mailing code because current send attempt (%d) is not less than given send attempt (%s)", int(sendAttempt), int(valSession.sendAttemptNumber))
            return valSession.id

        ipstring = ipaddress if ipaddress else u"an unknown location"

        substitutions = {
            'ipaddress': ipstring,
            'link': self.makeValidateLink(valSession, clientSecret, nextLink),
            'token': valSession.token,
        }
        logger.info(
            "Attempting to mail code %s (nextLink: %s) to %s",
            valSession.token, nextLink, emailAddress,
        )
        sendEmail(self.sydent, 'email.template', emailAddress, substitutions)

        valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt)

        return valSession.id
Example #21
0
    async def requestToken(
        self,
        phoneNumber: phonenumbers.PhoneNumber,
        clientSecret: str,
        send_attempt: int,
        brand: Optional[str] = None,
    ) -> int:
        """
        Creates or retrieves a validation session and sends an text message to the
        corresponding phone number address with a token to use to verify the association.

        :param phoneNumber: The phone number to send the email to.
        :param clientSecret: The client secret to use.
        :param send_attempt: The current send attempt.
        :param brand: A hint at a brand from the request.

        :return: The ID of the session created (or of the existing one if any)
        """
        if str(phoneNumber.country_code) in self.smsRules:
            action = self.smsRules[str(phoneNumber.country_code)]
            if action == "reject":
                raise DestinationRejectedException()

        valSessionStore = ThreePidValSessionStore(self.sydent)

        msisdn = phonenumbers.format_number(
            phoneNumber, phonenumbers.PhoneNumberFormat.E164)[1:]

        valSession, token_info = valSessionStore.getOrCreateTokenSession(
            medium="msisdn", address=msisdn, clientSecret=clientSecret)

        valSessionStore.setMtime(valSession.id, time_msec())

        if token_info.send_attempt_number >= send_attempt:
            logger.info(
                "Not texting code because current send attempt (%d) is not less than given send attempt (%s)",
                send_attempt,
                token_info.send_attempt_number,
            )
            return valSession.id

        smsBodyTemplate = self.sydent.config.sms.body_template
        originator = self.getOriginator(phoneNumber)

        logger.info(
            "Attempting to text code %s to %s (country %d) with originator %s",
            token_info.token,
            msisdn,
            phoneNumber.country_code,
            originator,
        )

        smsBody = smsBodyTemplate.format(token=token_info.token)

        await self.omSms.sendTextSMS(smsBody, msisdn, originator)

        valSessionStore.setSendAttemptNumber(valSession.id, send_attempt)

        return valSession.id
Example #22
0
    def _pushSucceeded(self, result, peer, maxId):
        logger.info("Pushed updates up to %d to %s with result %d %s",
                    maxId, peer.servername, result.code, result.phrase)

        self.peerStore.setLastSentVersionAndPokeSucceeded(peer.servername, maxId, time_msec())

        self.pushing = False
        self.scheduledPush()
Example #23
0
def sendEmail(sydent, templateName, mailTo, substitutions):
    mailFrom = sydent.cfg.get('email', 'email.from')
    mailTemplateFile = sydent.cfg.get('email', templateName)

    myHostname = sydent.cfg.get('email', 'email.hostname')
    if myHostname == '':
        myHostname = socket.getfqdn()
    midRandom = "".join(
        [random.choice(string.ascii_letters) for _ in range(16)])
    messageid = "<%d%s@%s>" % (time_msec(), midRandom, myHostname)

    allSubstitutions = {}
    allSubstitutions.update(substitutions)
    allSubstitutions.update({
        'messageid': messageid,
        'date': email.utils.formatdate(localtime=False),
        'to': mailTo,
        'from': mailFrom,
    })

    for k, v in allSubstitutions.items():
        allSubstitutions[k] = v.decode('utf8')
        allSubstitutions[k + "_forhtml"] = cgi.escape(v.decode('utf8'))
        allSubstitutions[k + "_forurl"] = urllib.quote(v)

    mailString = open(mailTemplateFile).read() % allSubstitutions
    rawFrom = email.utils.parseaddr(mailFrom)[1]
    rawTo = email.utils.parseaddr(mailTo)[1]
    if rawFrom == '' or rawTo == '':
        logger.info("Couldn't parse from / to address %s / %s", mailFrom,
                    mailTo)
        raise EmailAddressException()
    mailServer = sydent.cfg.get('email', 'email.smtphost')
    mailPort = sydent.cfg.get('email', 'email.smtpport')
    mailUsername = sydent.cfg.get('email', 'email.smtpusername')
    mailPassword = sydent.cfg.get('email', 'email.smtppassword')
    mailTLSMode = sydent.cfg.get('email', 'email.tlsmode')
    logger.info("Sending mail to %s with mail server: %s" % (
        mailTo,
        mailServer,
    ))
    try:
        if mailTLSMode == 'SSL' or mailTLSMode == 'TLS':
            smtp = smtplib.SMTP_SSL(mailServer, mailPort, myHostname)
        elif mailTLSMode == 'STARTTLS':
            smtp = smtplib.SMTP(mailServer, mailPort, myHostname)
            smtp.starttls()
        else:
            smtp = smtplib.SMTP(mailServer, mailPort, myHostname)
        if mailUsername != '':
            smtp.login(mailUsername, mailPassword)
        smtp.sendmail(rawFrom, rawTo, mailString.encode('utf-8'))
        smtp.quit()
    except Exception as origException:
        twisted.python.log.err()
        ese = EmailSendException()
        ese.cause = origException
        raise ese
Example #24
0
    def _pushSucceeded(self, result, peer, maxId):
        logger.info("Pushed updates up to %d to %s with result %d %s", maxId,
                    peer.servername, result.code, result.phrase)

        self.peerStore.setLastSentVersionAndPokeSucceeded(
            peer.servername, maxId, time_msec())

        self.pushing = False
        self.scheduledPush()
Example #25
0
    def requestToken(self,
                     emailAddress,
                     clientSecret,
                     sendAttempt,
                     nextLink,
                     ipaddress=None):
        """
        Creates or retrieves a validation session and sends an email to the corresponding
        email address with a token to use to verify the association.

        :param emailAddress: The email address to send the email to.
        :type emailAddress: unicode
        :param clientSecret: The client secret to use.
        :type clientSecret: unicode
        :param sendAttempt: The current send attempt.
        :type sendAttempt: int
        :param nextLink: The link to redirect the user to once they have completed the
            validation.
        :type nextLink: unicode
        :param ipaddress: The requester's IP address.
        :type ipaddress: str or None

        :return: The ID of the session created (or of the existing one if any)
        :rtype: int
        """
        valSessionStore = ThreePidValSessionStore(self.sydent)

        valSession = valSessionStore.getOrCreateTokenSession(
            medium=u'email', address=emailAddress, clientSecret=clientSecret)

        valSessionStore.setMtime(valSession.id, time_msec())

        if int(valSession.sendAttemptNumber) >= int(sendAttempt):
            logger.info(
                "Not mailing code because current send attempt (%d) is not less than given send attempt (%s)",
                int(sendAttempt), int(valSession.sendAttemptNumber))
            return valSession.id

        ipstring = ipaddress if ipaddress else u"an unknown location"

        substitutions = {
            'ipaddress': ipstring,
            'link': self.makeValidateLink(valSession, clientSecret, nextLink),
            'token': valSession.token,
        }
        logger.info(
            "Attempting to mail code %s (nextLink: %s) to %s",
            valSession.token,
            nextLink,
            emailAddress,
        )
        sendEmail(self.sydent, 'email.template', emailAddress, substitutions)

        valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt)

        return valSession.id
Example #26
0
    def signedAssociationStringForThreepid(self, medium, address):
        cur = self.sydent.db.cursor()
        # We treat address as case-insensitive because that's true for all the threepids
        # we have currently (we treat the local part of email addresses as case insensitive
        # which is technically incorrect). If we someday get a case-sensitive threepid,
        # this can change.
        res = cur.execute("select sgAssoc from global_threepid_associations where "
                    "medium = ? and lower(address) = lower(?) and notBefore < ? and notAfter > ? "
                    "order by ts desc limit 1",
                    (medium, address, time_msec(), time_msec()))

        row = res.fetchone()

        if not row:
            return None

        sgAssocBytes = row[0]

        return sgAssocBytes
Example #27
0
    def getMxids(self, threepid_tuples):
        cur = self.sydent.db.cursor()

        cur.execute(
            "CREATE TEMPORARY TABLE tmp_getmxids (medium VARCHAR(16), address VARCHAR(256))"
        )
        cur.execute(
            "CREATE INDEX tmp_getmxids_medium_lower_address ON tmp_getmxids (medium, lower(address))"
        )

        try:
            inserted_cap = 0
            while inserted_cap < len(threepid_tuples):
                cur.executemany(
                    "INSERT INTO tmp_getmxids (medium, address) VALUES (?, ?)",
                    threepid_tuples[inserted_cap:inserted_cap + 500])
                inserted_cap += 500

            res = cur.execute(
                # 'notBefore' is the time the association starts being valid, 'notAfter' the the time at which
                # it ceases to be valid, so the ts must be greater than 'notBefore' and less than 'notAfter'.
                "SELECT gte.medium, gte.address, gte.ts, gte.mxid FROM global_threepid_associations gte "
                "JOIN tmp_getmxids ON gte.medium = tmp_getmxids.medium AND lower(gte.address) = lower(tmp_getmxids.address) "
                "WHERE gte.notBefore < ? AND gte.notAfter > ? "
                "ORDER BY gte.medium, gte.address, gte.ts DESC",
                (time_msec(), time_msec()))

            results = []
            current = ()
            for row in res.fetchall():
                # only use the most recent entry for each
                # threepid (they're sorted by ts)
                if (row[0], row[1]) == current:
                    continue
                current = (row[0], row[1])
                results.append((row[0], row[1], row[3]))

        finally:
            res = cur.execute("DROP TABLE tmp_getmxids")

        return results
Example #28
0
    def requestToken(self, phoneNumber, clientSecret, sendAttempt, brand=None):
        """
        Creates or retrieves a validation session and sends an text message to the
        corresponding phone number address with a token to use to verify the association.

        :param phoneNumber: The phone number to send the email to.
        :type phoneNumber: phonenumbers.PhoneNumber
        :param clientSecret: The client secret to use.
        :type clientSecret: unicode
        :param sendAttempt: The current send attempt.
        :type sendAttempt: int
        :param brand: A hint at a brand from the request.
        :type brand: str or None

        :return: The ID of the session created (or of the existing one if any)
        :rtype: int
        """
        if str(phoneNumber.country_code) in self.smsRules:
            action = self.smsRules[str(phoneNumber.country_code)]
            if action == 'reject':
                raise DestinationRejectedException()

        valSessionStore = ThreePidValSessionStore(self.sydent)

        msisdn = phonenumbers.format_number(
            phoneNumber, phonenumbers.PhoneNumberFormat.E164
        )[1:]

        valSession = valSessionStore.getOrCreateTokenSession(
            medium='msisdn', address=msisdn, clientSecret=clientSecret
        )

        valSessionStore.setMtime(valSession.id, time_msec())

        if int(valSession.sendAttemptNumber) >= int(sendAttempt):
            logger.info("Not texting code because current send attempt (%d) is not less than given send attempt (%s)", int(sendAttempt), int(valSession.sendAttemptNumber))
            return valSession.id

        smsBodyTemplate = self.sydent.cfg.get('sms', 'bodyTemplate')
        originator = self.getOriginator(phoneNumber)

        logger.info(
            "Attempting to text code %s to %s (country %d) with originator %s",
            valSession.token, msisdn, phoneNumber.country_code, originator
        )

        smsBody = smsBodyTemplate.format(token=valSession.token)

        self.omSms.sendTextSMS(smsBody, msisdn, originator)

        valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt)

        return valSession.id
Example #29
0
    async def _push_to_peer(self, p: "RemotePeer") -> None:
        """
        For a given peer, retrieves the list of associations that were created since
        the last successful push to this peer (limited to ASSOCIATIONS_PUSH_LIMIT) and
        sends them.

        :param p: The peer to send associations to.
        """
        logger.debug("Looking for updates to push to %s", p.servername)

        # Check if a push operation is already active. If so, don't start another
        if p.is_being_pushed_to:
            logger.debug(
                "Waiting for %s to finish pushing...", p.replication_url_origin
            )
            return

        p.is_being_pushed_to = True

        try:
            # Push associations
            (
                assocs,
                latest_assoc_id,
            ) = self.local_assoc_store.getSignedAssociationsAfterId(
                p.lastSentVersion, ASSOCIATIONS_PUSH_LIMIT
            )

            # If there are no updates left to send, break the loop
            if not assocs:
                return

            logger.info(
                "Pushing %d updates to %s", len(assocs), p.replication_url_origin
            )
            result = await p.pushUpdates(assocs)

            self.peerStore.setLastSentVersionAndPokeSucceeded(
                p.servername, latest_assoc_id, time_msec()
            )

            logger.info(
                "Pushed updates to %s with result %d %s",
                p.replication_url_origin,
                result.code,
                result.phrase,
            )
        except Exception:
            logger.exception("Error pushing updates to %s", p.replication_url_origin)
        finally:
            # Whether pushing completed or an error occurred, signal that pushing has finished
            p.is_being_pushed_to = False
    def getMxids(self, threepid_tuples):
        cur = self.sydent.db.cursor()

        cur.execute("CREATE TEMPORARY TABLE tmp_getmxids (medium VARCHAR(16), address VARCHAR(256))");
        cur.execute("CREATE INDEX tmp_getmxids_medium_lower_address ON tmp_getmxids (medium, lower(address))");

        try:
            inserted_cap = 0
            while inserted_cap < len(threepid_tuples):
                cur.executemany(
                    "INSERT INTO tmp_getmxids (medium, address) VALUES (?, ?)",
                    threepid_tuples[inserted_cap:inserted_cap + 500]
                )
                inserted_cap += 500

            res = cur.execute(
                # 'notBefore' is the time the association starts being valid, 'notAfter' the the time at which
                # it ceases to be valid, so the ts must be greater than 'notBefore' and less than 'notAfter'.
                "SELECT gte.medium, gte.address, gte.ts, gte.mxid FROM global_threepid_associations gte "
                "JOIN tmp_getmxids ON gte.medium = tmp_getmxids.medium AND lower(gte.address) = lower(tmp_getmxids.address) "
                "WHERE gte.notBefore < ? AND gte.notAfter > ? "
                "ORDER BY gte.medium, gte.address, gte.ts DESC",
                (time_msec(), time_msec())
            )

            results = []
            current = ()
            for row in res.fetchall():
                # only use the most recent entry for each
                # threepid (they're sorted by ts)
                if (row[0], row[1]) == current:
                    continue
                current = (row[0], row[1])
                results.append((row[0], row[1], row[3]))

        finally:
            res = cur.execute("DROP TABLE tmp_getmxids")

        return results
Example #31
0
def validateSessionWithToken(sydent, sid, clientSecret, token):
    """
    Attempt to validate a session, identified by the sid, using
    the token from out-of-band. The client secret is given to
    prevent attempts to guess the token for a sid.

    :param sid: The ID of the session to validate.
    :type sid: unicode
    :param clientSecret: The client secret to validate.
    :type clientSecret: unicode
    :param token: The token to validate.
    :type token: unicode

    :return: A dict with a "success" key which is True if the session
        was successfully validated, False otherwise.
    :rtype: dict[str, bool]

    :raise IncorrectClientSecretException: The provided client_secret is incorrect.
    :raise SessionExpiredException: The session has expired.
    :raise InvalidSessionIdException: The session ID couldn't be matched with an
        existing session.
    :raise IncorrectSessionTokenException: The provided token is incorrect
    """
    valSessionStore = ThreePidValSessionStore(sydent)
    s = valSessionStore.getTokenSessionById(sid)
    if not s:
        logger.info("Session ID %s not found", sid)
        raise InvalidSessionIdException()

    if not clientSecret == s.clientSecret:
        logger.info("Incorrect client secret", sid)
        raise IncorrectClientSecretException()

    if s.mtime + ValidationSession.THREEPID_SESSION_VALIDATION_TIMEOUT_MS < time_msec(
    ):
        logger.info("Session expired")
        raise SessionExpiredException()

    # TODO once we can validate the token oob
    #if tokenObj.validated and clientSecret == tokenObj.clientSecret:
    #    return True

    if s.token == token:
        logger.info("Setting session %s as validated", s.id)
        valSessionStore.setValidated(s.id, True)

        return {'success': True}
    else:
        logger.info("Incorrect token submitted")
        raise IncorrectSessionTokenException()
    def removeAssociation(self, threepid: Dict[str, str], mxid: str) -> None:
        """
        Delete the association between a 3PID and a MXID, if it exists. If the
        association doesn't exist, log and do nothing. Please note that email
        addresses must be casefolded before calling this function.

        :param threepid: The 3PID of the binding to remove.
        :param mxid: The MXID of the binding to remove.
        """

        cur = self.sydent.db.cursor()

        # check to see if we have any matching associations first.
        # We use a REPLACE INTO because we need the resulting row to have
        # a new ID (such that we know it's a new change that needs to be
        # replicated) so there's no need to insert a deletion row if there's
        # nothing to delete.
        cur.execute(
            "SELECT COUNT(*) FROM local_threepid_associations "
            "WHERE medium = ? AND address = ? AND mxid = ?",
            (threepid["medium"], threepid["address"], mxid),
        )
        row: Tuple[int] = cur.fetchone()
        if row[0] > 0:
            ts = time_msec()
            cur.execute(
                "REPLACE INTO local_threepid_associations "
                "('medium', 'address', 'mxid', 'ts', 'notBefore', 'notAfter') "
                " values (?, ?, NULL, ?, null, null)",
                (threepid["medium"], threepid["address"], ts),
            )
            logger.info(
                "Deleting local assoc for %s/%s/%s replaced %d rows",
                threepid["medium"],
                threepid["address"],
                mxid,
                cur.rowcount,
            )
            self.sydent.db.commit()
        else:
            logger.info(
                "No local assoc found for %s/%s/%s",
                threepid["medium"],
                threepid["address"],
                mxid,
            )
Example #33
0
    def addBinding(self, medium, address, mxid):
        """Binds the given 3pid to the given mxid.

        It's assumed that we have somehow validated that the given user owns
        the given 3pid

        Args:
            medium (str): the type of 3pid
            address (str): the 3pid
            mxid (str): the mxid to bind it to
        """
        localAssocStore = LocalAssociationStore(self.sydent)

        createdAt = time_msec()
        expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS
        assoc = ThreepidAssociation(medium, address, mxid, createdAt,
                                    createdAt, expires)

        localAssocStore.addOrUpdateAssociation(assoc)

        self.sydent.pusher.doLocalPush()

        joinTokenStore = JoinTokenStore(self.sydent)
        pendingJoinTokens = joinTokenStore.getTokens(medium, address)
        invites = []
        for token in pendingJoinTokens:
            token["mxid"] = mxid
            token["signed"] = {
                "mxid": mxid,
                "token": token["token"],
            }
            token["signed"] = signedjson.sign.sign_json(
                token["signed"], self.sydent.server_name,
                self.sydent.keyring.ed25519)
            invites.append(token)
        if invites:
            assoc.extra_fields["invites"] = invites
            joinTokenStore.markTokensAsSent(medium, address)

        signer = Signer(self.sydent)
        sgassoc = signer.signedThreePidAssociation(assoc)

        self._notify(sgassoc, 0)

        return sgassoc
Example #34
0
    def addBinding(self, valSessionId, clientSecret, mxid):
        valSessionStore = ThreePidValSessionStore(self.sydent)
        localAssocStore = LocalAssociationStore(self.sydent)

        s = valSessionStore.getValidatedSession(valSessionId, clientSecret)

        createdAt = time_msec()
        expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS

        assoc = ThreepidAssociation(s.medium, s.address, mxid, createdAt, createdAt, expires)

        localAssocStore.addOrUpdateAssociation(assoc)

        self.sydent.pusher.doLocalPush()

        assocSigner = AssociationSigner(self.sydent)
        sgassoc = assocSigner.signedThreePidAssociation(assoc)

        return sgassoc
Example #35
0
    def getValidatedSession(self, sid, clientSecret):
        """
        Retrieve a validated and still-valid session whose client secret matches the one passed in
        """
        s = self.getSessionById(sid)

        if not s:
            raise InvalidSessionIdException()

        if not s.clientSecret == clientSecret:
            raise IncorrectClientSecretException()

        if s.mtime + ValidationSession.THREEPID_SESSION_VALID_LIFETIME_MS < time_msec():
            raise SessionExpiredException()

        if not s.validated:
            raise SessionNotValidatedException()

        return s
Example #36
0
    def addBinding(self, medium, address, mxid):
        """Binds the given 3pid to the given mxid.

        It's assumed that we have somehow validated that the given user owns
        the given 3pid

        Args:
            medium (str): the type of 3pid
            address (str): the 3pid
            mxid (str): the mxid to bind it to
        """
        localAssocStore = LocalAssociationStore(self.sydent)

        createdAt = time_msec()
        expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS
        assoc = ThreepidAssociation(medium, address, mxid, createdAt, createdAt, expires)

        localAssocStore.addOrUpdateAssociation(assoc)

        self.sydent.pusher.doLocalPush()

        joinTokenStore = JoinTokenStore(self.sydent)
        pendingJoinTokens = joinTokenStore.getTokens(medium, address)
        invites = []
        for token in pendingJoinTokens:
            token["mxid"] = mxid
            token["signed"] = {
                "mxid": mxid,
                "token": token["token"],
            }
            token["signed"] = signedjson.sign.sign_json(token["signed"], self.sydent.server_name, self.sydent.keyring.ed25519)
            invites.append(token)
        if invites:
            assoc.extra_fields["invites"] = invites
            joinTokenStore.markTokensAsSent(medium, address)

        signer = Signer(self.sydent)
        sgassoc = signer.signedThreePidAssociation(assoc)

        self._notify(sgassoc, 0)

        return sgassoc
Example #37
0
    def getValidatedSession(self, sid, clientSecret):
        """
        Retrieve a validated and still-valid session whose client secret matches the one passed in
        """
        s = self.getSessionById(sid)

        if not s:
            raise InvalidSessionIdException()

        if not s.clientSecret == clientSecret:
            raise IncorrectClientSecretException()

        if s.mtime + ValidationSession.THREEPID_SESSION_VALID_LIFETIME_MS < time_msec(
        ):
            raise SessionExpiredException()

        if not s.validated:
            raise SessionNotValidatedException()

        return s
Example #38
0
    def deleteOldSessions(self) -> None:
        """Delete old threepid validation sessions that are long expired."""

        cur = self.sydent.db.cursor()

        delete_before_ts = time_msec() - 5 * THREEPID_SESSION_VALID_LIFETIME_MS

        sql = """
            DELETE FROM threepid_validation_sessions
            WHERE mtime < ?
        """
        cur.execute(sql, (delete_before_ts, ))

        sql = """
            DELETE FROM threepid_token_auths
            WHERE validationSession NOT IN (
                SELECT id FROM threepid_validation_sessions
            )
        """
        cur.execute(sql)

        self.sydent.db.commit()
Example #39
0
    def addBinding(self, valSessionId, clientSecret, mxid):
        valSessionStore = ThreePidValSessionStore(self.sydent)
        localAssocStore = LocalAssociationStore(self.sydent)

        s = valSessionStore.getValidatedSession(valSessionId, clientSecret)

        createdAt = time_msec()
        expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS

        assoc = ThreepidAssociation(s.medium, s.address, mxid, createdAt,
                                    createdAt, expires)

        localAssocStore.addOrUpdateAssociation(assoc)

        self.sydent.pusher.doLocalPush()

        joinTokenStore = JoinTokenStore(self.sydent)
        pendingJoinTokens = joinTokenStore.getTokens(s.medium, s.address)
        invites = []
        for token in pendingJoinTokens:
            token["mxid"] = mxid
            token["signed"] = {
                "mxid": mxid,
                "token": token["token"],
            }
            token["signed"] = signedjson.sign.sign_json(
                token["signed"], self.sydent.server_name,
                self.sydent.keyring.ed25519)
            invites.append(token)
        if invites:
            assoc.extra_fields["invites"] = invites
            joinTokenStore.markTokensAsSent(s.medium, s.address)

        assocSigner = AssociationSigner(self.sydent)
        sgassoc = assocSigner.signedThreePidAssociation(assoc)

        self._notify(sgassoc, 0)

        return sgassoc
Example #40
0
def sendEmail(sydent, templateName, mailTo, substitutions):
    mailFrom = sydent.cfg.get('email', 'email.from')
    mailTemplateFile = sydent.cfg.get('email', templateName)

    myHostname = os.uname()[1]
    midRandom = "".join(
        [random.choice(string.ascii_letters) for _ in range(16)])
    messageid = "<%d%s@%s>" % (time_msec(), midRandom, myHostname)

    allSubstitutions = {}
    allSubstitutions.update(substitutions)
    allSubstitutions.update({
        'messageid': messageid,
        'date': email.utils.formatdate(localtime=False),
        'to': mailTo,
        'from': mailFrom,
    })
    mailString = open(mailTemplateFile).read() % allSubstitutions
    rawFrom = email.utils.parseaddr(mailFrom)[1]
    rawTo = email.utils.parseaddr(mailTo)[1]
    if rawFrom == '' or rawTo == '':
        logger.info("Couldn't parse from / to address %s / %s", mailFrom,
                    mailTo)
        raise EmailAddressException()
    mailServer = sydent.cfg.get('email', 'email.smtphost')
    logger.info("Sending mail to %s with mail server: %s" % (
        mailTo,
        mailServer,
    ))
    try:
        smtp = smtplib.SMTP(mailServer)
        smtp.sendmail(rawFrom, rawTo, mailString)
        smtp.quit()
    except Exception as origException:
        twisted.python.log.err()
        ese = EmailSendException()
        ese.cause = origException
        raise ese
Example #41
0
    def deleteOldSessions(self):
        """Delete old threepid validation sessions that are long expired.
        """

        cur = self.sydent.db.cursor()

        delete_before_ts = time_msec() - 5 * ValidationSession.THREEPID_SESSION_VALID_LIFETIME_MS

        sql = """
            DELETE FROM threepid_validation_sessions
            WHERE mtime < ?
        """
        cur.execute(sql, (delete_before_ts,))

        sql = """
            DELETE FROM threepid_token_auths
            WHERE validationSession NOT IN (
                SELECT id FROM threepid_validation_sessions
            )
        """
        cur.execute(sql)

        self.sydent.db.commit()
Example #42
0
    def requestToken(self,
                     emailAddress,
                     clientSecret,
                     sendAttempt,
                     nextLink,
                     ipaddress=None):
        valSessionStore = ThreePidValSessionStore(self.sydent)

        valSession = valSessionStore.getOrCreateTokenSession(
            medium='email', address=emailAddress, clientSecret=clientSecret)

        valSessionStore.setMtime(valSession.id, time_msec())

        if int(valSession.sendAttemptNumber) >= int(sendAttempt):
            logger.info(
                "Not mailing code because current send attempt (%d) is not less than given send attempt (%s)",
                int(sendAttempt), int(valSession.sendAttemptNumber))
            return valSession.id

        ipstring = ipaddress if ipaddress else u"an unknown location"

        substitutions = {
            'ipaddress': ipstring,
            'link': self.makeValidateLink(valSession, clientSecret, nextLink),
            'token': valSession.token,
        }
        logger.info(
            "Attempting to mail code %s (nextLink: %s) to %s",
            valSession.token,
            nextLink,
            emailAddress,
        )
        sendEmail(self.sydent, 'email.template', emailAddress, substitutions)

        valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt)

        return valSession.id
Example #43
0
    def requestToken(self, phoneNumber, clientSecret, sendAttempt, nextLink):
        if str(phoneNumber.country_code) in self.smsRules:
            action = self.smsRules[str(phoneNumber.country_code)]
            if action == 'reject':
                raise DestinationRejectedException()

        valSessionStore = ThreePidValSessionStore(self.sydent)

        msisdn = phonenumbers.format_number(
            phoneNumber, phonenumbers.PhoneNumberFormat.E164
        )[1:]

        valSession = valSessionStore.getOrCreateTokenSession(
            medium='msisdn', address=msisdn, clientSecret=clientSecret
        )

        valSessionStore.setMtime(valSession.id, time_msec())

        if int(valSession.sendAttemptNumber) >= int(sendAttempt):
            logger.info("Not texting code because current send attempt (%d) is not less than given send attempt (%s)", int(sendAttempt), int(valSession.sendAttemptNumber))
            return valSession.id

        smsBodyTemplate = self.sydent.cfg.get('sms', 'bodyTemplate')
        originator = self.getOriginator(phoneNumber)

        logger.info(
            "Attempting to text code %s to %s (country %d) with originator %s",
            valSession.token, msisdn, phoneNumber.country_code, originator
        )

        smsBody = smsBodyTemplate.format(token=valSession.token)

        self.omSms.sendTextSMS(smsBody, msisdn, originator)

        valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt)

        return valSession.id
Example #44
0
    def addBinding(self, valSessionId, clientSecret, mxid):
        valSessionStore = ThreePidValSessionStore(self.sydent)
        localAssocStore = LocalAssociationStore(self.sydent)

        s = valSessionStore.getValidatedSession(valSessionId, clientSecret)

        createdAt = time_msec()
        expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS

        assoc = ThreepidAssociation(s.medium, s.address, mxid, createdAt, createdAt, expires)

        localAssocStore.addOrUpdateAssociation(assoc)

        self.sydent.pusher.doLocalPush()

        joinTokenStore = JoinTokenStore(self.sydent)
        pendingJoinTokens = joinTokenStore.getTokens(s.medium, s.address)
        invites = []
        for token in pendingJoinTokens:
            token["mxid"] = mxid
            token["signed"] = {
                "mxid": mxid,
                "token": token["token"],
            }
            token["signed"] = signedjson.sign.sign_json(token["signed"], self.sydent.server_name, self.sydent.keyring.ed25519)
            invites.append(token)
        if invites:
            assoc.extra_fields["invites"] = invites
            joinTokenStore.markTokensAsSent(s.medium, s.address)

        assocSigner = AssociationSigner(self.sydent)
        sgassoc = assocSigner.signedThreePidAssociation(assoc)

        self._notify(sgassoc, 0)

        return sgassoc
Example #45
0
def sendEmail(sydent, templateName, mailTo, substitutions):
        mailFrom = sydent.cfg.get('email', 'email.from')
        mailTemplateFile = sydent.cfg.get('email', templateName)

        myHostname = sydent.cfg.get('email', 'email.hostname')
        if myHostname == '':
            myHostname = socket.getfqdn()
        midRandom = "".join([random.choice(string.ascii_letters) for _ in range(16)])
        messageid = "<%d%s@%s>" % (time_msec(), midRandom, myHostname)

        allSubstitutions = {}
        allSubstitutions.update(substitutions)
        allSubstitutions.update({
            'messageid': messageid,
            'date': email.utils.formatdate(localtime=False),
            'to': mailTo,
            'from': mailFrom,
        })

        for k,v in allSubstitutions.items():
            allSubstitutions[k] = v.decode('utf8')
            allSubstitutions[k+"_forhtml"] = cgi.escape(v.decode('utf8'))
            allSubstitutions[k+"_forurl"] = urllib.quote(v)

        mailString = open(mailTemplateFile).read().decode('utf8') % allSubstitutions
        parsedFrom = email.utils.parseaddr(mailFrom)[1]
        parsedTo = email.utils.parseaddr(mailTo)[1]
        if parsedFrom == '' or parsedTo == '':
            logger.info("Couldn't parse from / to address %s / %s", mailFrom, mailTo)
            raise EmailAddressException()

        if parsedTo != mailTo:
            logger.info("Parsed to address changed the address: %s -> %s", mailTo, parsedTo)
            raise EmailAddressException()

        mailServer = sydent.cfg.get('email', 'email.smtphost')
        mailPort = sydent.cfg.get('email', 'email.smtpport')
        mailUsername = sydent.cfg.get('email', 'email.smtpusername')
        mailPassword = sydent.cfg.get('email', 'email.smtppassword')
        mailTLSMode = sydent.cfg.get('email', 'email.tlsmode')
        logger.info("Sending mail to %s with mail server: %s" % (mailTo, mailServer,))
        try:
            if mailTLSMode == 'SSL' or mailTLSMode == 'TLS':
                smtp = smtplib.SMTP_SSL(mailServer, mailPort, myHostname)
            elif mailTLSMode == 'STARTTLS':
                smtp = smtplib.SMTP(mailServer, mailPort, myHostname)
                smtp.starttls()
            else:
                smtp = smtplib.SMTP(mailServer, mailPort, myHostname)
            if mailUsername != '':
                smtp.login(mailUsername, mailPassword)

            # We're using the parsing above to do basic validation, but instead of
            # failing it may munge the address it returns. So we should *not* use
            # that parsed address, as it may not match any validation done
            # elsewhere.
            smtp.sendmail(mailFrom, mailTo, mailString.encode('utf-8'))
            smtp.quit()
        except Exception as origException:
            twisted.python.log.err()
            ese = EmailSendException()
            ese.cause = origException
            raise ese