merge bookmark @
This commit is contained in:
commit
14d00a9dda
8 changed files with 327 additions and 201 deletions
|
|
@ -303,7 +303,11 @@ class SatXMPPEntity(core_types.SatXMPPEntity):
|
|||
self.streamInitialized()
|
||||
|
||||
def _finish_connection(self, __):
|
||||
self.conn_deferred.callback(None)
|
||||
if self.conn_deferred.called:
|
||||
# can happen in case of forced disconnection by server
|
||||
log.debug(f"{self} has already been connected")
|
||||
else:
|
||||
self.conn_deferred.callback(None)
|
||||
|
||||
def streamInitialized(self):
|
||||
"""Called after _authd"""
|
||||
|
|
|
|||
|
|
@ -545,7 +545,7 @@ class FileSharing:
|
|||
self._f.openFileWrite(
|
||||
client, file_tmp_path, transfer_data, file_data, stream_object
|
||||
)
|
||||
return False, defer.succeed(True)
|
||||
return False, True
|
||||
|
||||
async def _retrieveFiles(
|
||||
self, client, session, content_data, content_name, file_data, file_elt
|
||||
|
|
|
|||
|
|
@ -428,6 +428,7 @@ class Events(object):
|
|||
)
|
||||
else:
|
||||
timestamp, data = self._parseEventElt(event_elt)
|
||||
data["interest_id"] = item["id"]
|
||||
events.append((timestamp, data))
|
||||
defer.returnValue(events)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import shortuuid
|
||||
from typing import Optional
|
||||
from twisted.internet import defer
|
||||
from twisted.words.protocols.jabber import jid
|
||||
from twisted.words.protocols.jabber import error
|
||||
|
|
@ -131,6 +132,97 @@ class InvitationsPlugin(object):
|
|||
kwargs[key] = str(value)
|
||||
return defer.ensureDeferred(self.create(**kwargs))
|
||||
|
||||
async def getExistingInvitation(self, email: Optional[str]) -> Optional[dict]:
|
||||
"""Retrieve existing invitation with given email
|
||||
|
||||
@param email: check if any invitation exist with this email
|
||||
@return: first found invitation, or None if nothing found
|
||||
"""
|
||||
# FIXME: This method is highly inefficient, it get all invitations and check them
|
||||
# one by one, this is just a temporary way to avoid creating creating new accounts
|
||||
# for an existing email. A better way will be available with Libervia 0.9.
|
||||
# TODO: use a better way to check existing invitations
|
||||
|
||||
if email is None:
|
||||
return None
|
||||
all_invitations = await self.invitations.all()
|
||||
for id_, invitation in all_invitations.items():
|
||||
if invitation.get("email") == email:
|
||||
invitation[KEY_ID] = id_
|
||||
return invitation
|
||||
|
||||
async def _createAccountAndProfile(
|
||||
self,
|
||||
id_: str,
|
||||
kwargs: dict,
|
||||
extra: dict
|
||||
) -> None:
|
||||
"""Create XMPP account and Libervia profile for guest"""
|
||||
## XMPP account creation
|
||||
password = kwargs.pop('password', None)
|
||||
if password is None:
|
||||
password = utils.generatePassword()
|
||||
assert password
|
||||
# XXX: password is here saved in clear in database
|
||||
# it is needed for invitation as the same password is used for profile
|
||||
# and SàT need to be able to automatically open the profile with the uuid
|
||||
# FIXME: we could add an extra encryption key which would be used with the
|
||||
# uuid when the invitee is connecting (e.g. with URL). This key would
|
||||
# not be saved and could be used to encrypt profile password.
|
||||
extra[KEY_PASSWORD] = password
|
||||
|
||||
jid_ = kwargs.pop('jid_', None)
|
||||
if not jid_:
|
||||
domain = self.host.memory.getConfig(None, 'xmpp_domain')
|
||||
if not domain:
|
||||
# TODO: fallback to profile's domain
|
||||
raise ValueError(_("You need to specify xmpp_domain in sat.conf"))
|
||||
jid_ = "invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(),
|
||||
domain=domain)
|
||||
jid_ = jid.JID(jid_)
|
||||
extra[KEY_JID] = jid_.full()
|
||||
|
||||
if jid_.user:
|
||||
# we don't register account if there is no user as anonymous login is then
|
||||
# used
|
||||
try:
|
||||
await self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
|
||||
except error.StanzaError as e:
|
||||
prefix = jid_.user
|
||||
idx = 0
|
||||
while e.condition == 'conflict':
|
||||
if idx >= SUFFIX_MAX:
|
||||
raise exceptions.ConflictError(_("Can't create XMPP account"))
|
||||
jid_.user = prefix + '_' + str(idx)
|
||||
log.info(_("requested jid already exists, trying with {}".format(
|
||||
jid_.full())))
|
||||
try:
|
||||
await self.host.plugins['XEP-0077'].registerNewAccount(
|
||||
jid_,
|
||||
password
|
||||
)
|
||||
except error.StanzaError:
|
||||
idx += 1
|
||||
else:
|
||||
break
|
||||
if e.condition != 'conflict':
|
||||
raise e
|
||||
|
||||
log.info(_("account {jid_} created").format(jid_=jid_.full()))
|
||||
|
||||
## profile creation
|
||||
|
||||
extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(
|
||||
uuid=id_
|
||||
)
|
||||
# profile creation should not fail as we generate unique name ourselves
|
||||
await self.host.memory.createProfile(guest_profile, password)
|
||||
await self.host.memory.startSession(password, guest_profile)
|
||||
await self.host.memory.setParam("JabberID", jid_.full(), "Connection",
|
||||
profile_key=guest_profile)
|
||||
await self.host.memory.setParam("Password", password, "Connection",
|
||||
profile_key=guest_profile)
|
||||
|
||||
async def create(self, **kwargs):
|
||||
r"""Create an invitation
|
||||
|
||||
|
|
@ -201,6 +293,13 @@ class InvitationsPlugin(object):
|
|||
self.checkExtra(extra)
|
||||
|
||||
email = kwargs.pop('email', None)
|
||||
|
||||
existing = await self.getExistingInvitation(email)
|
||||
if existing is not None:
|
||||
log.info(f"There is already an invitation for {email!r}")
|
||||
extra.update(existing)
|
||||
del extra[KEY_ID]
|
||||
|
||||
emails_extra = kwargs.pop('emails_extra', [])
|
||||
if not email and emails_extra:
|
||||
raise ValueError(
|
||||
|
|
@ -214,67 +313,18 @@ class InvitationsPlugin(object):
|
|||
|
||||
## uuid
|
||||
log.info(_("creating an invitation"))
|
||||
id_ = str(shortuuid.uuid())
|
||||
id_ = existing[KEY_ID] if existing else str(shortuuid.uuid())
|
||||
|
||||
## XMPP account creation
|
||||
password = kwargs.pop('password', None)
|
||||
if password is None:
|
||||
password = utils.generatePassword()
|
||||
assert password
|
||||
# XXX: password is here saved in clear in database
|
||||
# it is needed for invitation as the same password is used for profile
|
||||
# and SàT need to be able to automatically open the profile with the uuid
|
||||
# FIXME: we could add an extra encryption key which would be used with the uuid
|
||||
# when the invitee is connecting (e.g. with URL). This key would not be
|
||||
# saved and could be used to encrypt profile password.
|
||||
extra[KEY_PASSWORD] = password
|
||||
if existing is None:
|
||||
await self._createAccountAndProfile(id_, kwargs, extra)
|
||||
|
||||
jid_ = kwargs.pop('jid_', None)
|
||||
if not jid_:
|
||||
domain = self.host.memory.getConfig(None, 'xmpp_domain')
|
||||
if not domain:
|
||||
# TODO: fallback to profile's domain
|
||||
raise ValueError(_("You need to specify xmpp_domain in sat.conf"))
|
||||
jid_ = "invitation-{uuid}@{domain}".format(uuid=shortuuid.uuid(),
|
||||
domain=domain)
|
||||
jid_ = jid.JID(jid_)
|
||||
if jid_.user:
|
||||
# we don't register account if there is no user as anonymous login is then
|
||||
# used
|
||||
try:
|
||||
await self.host.plugins['XEP-0077'].registerNewAccount(jid_, password)
|
||||
except error.StanzaError as e:
|
||||
prefix = jid_.user
|
||||
idx = 0
|
||||
while e.condition == 'conflict':
|
||||
if idx >= SUFFIX_MAX:
|
||||
raise exceptions.ConflictError(_("Can't create XMPP account"))
|
||||
jid_.user = prefix + '_' + str(idx)
|
||||
log.info(_("requested jid already exists, trying with {}".format(
|
||||
jid_.full())))
|
||||
try:
|
||||
await self.host.plugins['XEP-0077'].registerNewAccount(jid_,
|
||||
password)
|
||||
except error.StanzaError:
|
||||
idx += 1
|
||||
else:
|
||||
break
|
||||
if e.condition != 'conflict':
|
||||
raise e
|
||||
profile = kwargs.pop('profile', None)
|
||||
guest_profile = extra[KEY_GUEST_PROFILE]
|
||||
jid_ = jid.JID(extra[KEY_JID])
|
||||
|
||||
log.info(_("account {jid_} created").format(jid_=jid_.full()))
|
||||
|
||||
## profile creation
|
||||
|
||||
extra[KEY_GUEST_PROFILE] = guest_profile = INVITEE_PROFILE_TPL.format(uuid=id_)
|
||||
# profile creation should not fail as we generate unique name ourselves
|
||||
await self.host.memory.createProfile(guest_profile, password)
|
||||
await self.host.memory.startSession(password, guest_profile)
|
||||
await self.host.memory.setParam("JabberID", jid_.full(), "Connection",
|
||||
profile_key=guest_profile)
|
||||
await self.host.memory.setParam("Password", password, "Connection",
|
||||
profile_key=guest_profile)
|
||||
## identity
|
||||
name = kwargs.pop('name', None)
|
||||
password = extra[KEY_PASSWORD]
|
||||
if name is not None:
|
||||
extra['name'] = name
|
||||
try:
|
||||
|
|
@ -306,7 +356,6 @@ class InvitationsPlugin(object):
|
|||
else:
|
||||
format_args['name'] = name
|
||||
|
||||
profile = kwargs.pop('profile', None)
|
||||
if profile is None:
|
||||
format_args['profile'] = ''
|
||||
else:
|
||||
|
|
@ -344,8 +393,6 @@ class InvitationsPlugin(object):
|
|||
if kwargs:
|
||||
log.warning(_("Not all arguments have been consumed: {}").format(kwargs))
|
||||
|
||||
extra[KEY_JID] = jid_.full()
|
||||
|
||||
## extra data saving
|
||||
self.invitations[id_] = extra
|
||||
|
||||
|
|
|
|||
|
|
@ -17,16 +17,18 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
from html import escape
|
||||
from functools import partial
|
||||
from sat.core.i18n import _, D_
|
||||
from sat.core.constants import Const as C
|
||||
from sat.core.log import getLogger
|
||||
from html import escape
|
||||
import re
|
||||
from typing import Set
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet.threads import deferToThread
|
||||
|
||||
from sat.core import exceptions
|
||||
from sat.core.constants import Const as C
|
||||
from sat.core.i18n import D_, _
|
||||
from sat.core.log import getLogger
|
||||
from sat.tools import xml_tools
|
||||
|
||||
try:
|
||||
|
|
@ -115,7 +117,15 @@ VOID_ELEMENTS = (
|
|||
"track",
|
||||
"wbr")
|
||||
|
||||
SAFE_ATTRS = html.defs.safe_attrs.union(("style", "poster", "controls"))
|
||||
SAFE_ATTRS = html.defs.safe_attrs.union({"style", "poster", "controls"}) - {"id"}
|
||||
SAFE_CLASSES = {
|
||||
# those classes are used for code highlighting
|
||||
"bp", "c", "ch", "cm", "cp", "cpf", "cs", "dl", "err", "fm", "gd", "ge", "get", "gh",
|
||||
"gi", "go", "gp", "gr", "gs", "gt", "gu", "highlight", "hll", "il", "k", "kc", "kd",
|
||||
"kn", "kp", "kr", "kt", "m", "mb", "mf", "mh", "mi", "mo", "na", "nb", "nc", "nd",
|
||||
"ne", "nf", "ni", "nl", "nn", "no", "nt", "nv", "o", "ow", "s", "sa", "sb", "sc",
|
||||
"sd", "se", "sh", "si", "sr", "ss", "sx", "vc", "vg", "vi", "vm", "w", "write",
|
||||
}
|
||||
STYLES_VALUES_REGEX = (
|
||||
r"^("
|
||||
+ "|".join(
|
||||
|
|
@ -237,7 +247,8 @@ class TextSyntaxes(object):
|
|||
except ImportError:
|
||||
log.warning("markdown or html2text not found, can't use Markdown syntax")
|
||||
log.info(
|
||||
"You can download/install them from https://pythonhosted.org/Markdown/ and https://github.com/Alir3z4/html2text/"
|
||||
"You can download/install them from https://pythonhosted.org/Markdown/ "
|
||||
"and https://github.com/Alir3z4/html2text/"
|
||||
)
|
||||
host.bridge.addMethod(
|
||||
"syntaxConvert",
|
||||
|
|
@ -288,14 +299,14 @@ class TextSyntaxes(object):
|
|||
)
|
||||
return failure
|
||||
|
||||
def cleanStyle(self, styles):
|
||||
def cleanStyle(self, styles_raw: str) -> str:
|
||||
""""Clean unsafe CSS styles
|
||||
|
||||
Remove styles not in the whitelist, or where the value doesn't match the regex
|
||||
@param styles_raw(unicode): CSS styles
|
||||
@return (unicode): cleaned styles
|
||||
@param styles_raw: CSS styles
|
||||
@return: cleaned styles
|
||||
"""
|
||||
styles = styles.split(";")
|
||||
styles: List[str] = styles_raw.split(";")
|
||||
cleaned_styles = []
|
||||
for style in styles:
|
||||
try:
|
||||
|
|
@ -315,6 +326,14 @@ class TextSyntaxes(object):
|
|||
["%s: %s" % (key_, value_) for key_, value_ in cleaned_styles]
|
||||
)
|
||||
|
||||
def cleanClasses(self, classes_raw: str) -> str:
|
||||
"""Remove any non whitelisted class
|
||||
|
||||
@param classes_raw: classes set on an element
|
||||
@return: remaining classes (can be empty string)
|
||||
"""
|
||||
return " ".join(SAFE_CLASSES.intersection(classes_raw.split()))
|
||||
|
||||
def cleanXHTML(self, xhtml):
|
||||
"""Clean XHTML text by removing potentially dangerous/malicious parts
|
||||
|
||||
|
|
@ -341,6 +360,8 @@ class TextSyntaxes(object):
|
|||
xhtml_elt = cleaner.clean_html(xhtml_elt)
|
||||
for elt in xhtml_elt.xpath("//*[@style]"):
|
||||
elt.set("style", self.cleanStyle(elt.get("style")))
|
||||
for elt in xhtml_elt.xpath("//*[@class]"):
|
||||
elt.set("class", self.cleanClasses(elt.get("class")))
|
||||
# we remove self-closing elements for non-void elements
|
||||
for element in xhtml_elt.iter(tag=etree.Element):
|
||||
if not element.text:
|
||||
|
|
|
|||
|
|
@ -45,10 +45,7 @@ except:
|
|||
"Missing module pillow, please download/install it from https://python-pillow.github.io"
|
||||
)
|
||||
|
||||
try:
|
||||
from twisted.words.protocols.xmlstream import XMPPHandler
|
||||
except ImportError:
|
||||
from wokkel.subprotocols import XMPPHandler
|
||||
from twisted.words.protocols.jabber.xmlstream import XMPPHandler
|
||||
|
||||
IMPORT_NAME = "XEP-0054"
|
||||
|
||||
|
|
@ -109,7 +106,7 @@ class XEP_0054(object):
|
|||
NS_VCARD, client.profile)
|
||||
await client._xep_0054_avatar_hashes.load()
|
||||
|
||||
def savePhoto(self, client, photo_elt, entity_jid):
|
||||
def savePhoto(self, client, photo_elt, entity):
|
||||
"""Parse a <PHOTO> photo_elt and save the picture"""
|
||||
# XXX: this method is launched in a separate thread
|
||||
try:
|
||||
|
|
@ -131,7 +128,7 @@ class XEP_0054(object):
|
|||
raise Failure(exceptions.NotFound())
|
||||
|
||||
if not buf:
|
||||
log.warning("empty avatar for {jid}".format(jid=entity_jid.full()))
|
||||
log.warning("empty avatar for {jid}".format(jid=entity.full()))
|
||||
raise Failure(exceptions.NotFound())
|
||||
|
||||
log.debug(_("Decoding binary"))
|
||||
|
|
@ -140,7 +137,7 @@ class XEP_0054(object):
|
|||
|
||||
if mime_type is None:
|
||||
log.debug(
|
||||
f"no media type found specified for {entity_jid}'s avatar, trying to "
|
||||
f"no media type found specified for {entity}'s avatar, trying to "
|
||||
f"guess")
|
||||
|
||||
try:
|
||||
|
|
@ -149,7 +146,7 @@ class XEP_0054(object):
|
|||
log.warning(f"Can't open avatar buffer: {e}")
|
||||
|
||||
if mime_type is None:
|
||||
msg = f"Can't find media type for {entity_jid}'s avatar"
|
||||
msg = f"Can't find media type for {entity}'s avatar"
|
||||
log.warning(msg)
|
||||
raise Failure(exceptions.DataError(msg))
|
||||
|
||||
|
|
@ -435,19 +432,6 @@ class XEP_0054_handler(XMPPHandler):
|
|||
def getDiscoItems(self, requestor, target, nodeIdentifier=""):
|
||||
return []
|
||||
|
||||
def _checkAvatarHash(self, client, entity, given_hash):
|
||||
"""Check that hash in cache (i.e. computed hash) is the same as given one"""
|
||||
# XXX: if they differ, the avatar will be requested on each connection
|
||||
# TODO: try to avoid re-requesting avatar in this case
|
||||
computed_hash = client._xep_0054_avatar_hashes[entity.full()]
|
||||
if computed_hash != given_hash:
|
||||
log.warning(
|
||||
"computed hash differs from given hash for {entity}:\n"
|
||||
"computed: {computed}\ngiven: {given}".format(
|
||||
entity=entity, computed=computed_hash, given=given_hash
|
||||
)
|
||||
)
|
||||
|
||||
async def update(self, presence):
|
||||
"""Called on <presence/> stanza with vcard data
|
||||
|
||||
|
|
@ -468,19 +452,19 @@ class XEP_0054_handler(XMPPHandler):
|
|||
except StopIteration:
|
||||
return
|
||||
|
||||
new_hash = str(photo_elt).strip()
|
||||
if new_hash == HASH_SHA1_EMPTY:
|
||||
new_hash = ""
|
||||
given_hash = str(photo_elt).strip()
|
||||
if given_hash == HASH_SHA1_EMPTY:
|
||||
given_hash = ""
|
||||
|
||||
hashes_cache = client._xep_0054_avatar_hashes
|
||||
|
||||
old_hash = hashes_cache.get(entity_jid.full())
|
||||
|
||||
if old_hash == new_hash:
|
||||
if old_hash == given_hash:
|
||||
# no change, we can return…
|
||||
if new_hash:
|
||||
if given_hash:
|
||||
# …but we double check that avatar is in cache
|
||||
avatar_cache = self.host.common_cache.getMetadata(new_hash)
|
||||
avatar_cache = self.host.common_cache.getMetadata(given_hash)
|
||||
if avatar_cache is None:
|
||||
log.debug(
|
||||
f"Avatar for [{entity_jid}] is known but not in cache, we get "
|
||||
|
|
@ -492,19 +476,19 @@ class XEP_0054_handler(XMPPHandler):
|
|||
log.debug(f"avatar for {entity_jid} is already in cache")
|
||||
return
|
||||
|
||||
if new_hash is None:
|
||||
if given_hash is None:
|
||||
# XXX: we use empty string to indicate that there is no avatar
|
||||
new_hash = ""
|
||||
given_hash = ""
|
||||
|
||||
await hashes_cache.aset(entity_jid.full(), new_hash)
|
||||
await hashes_cache.aset(entity_jid.full(), given_hash)
|
||||
|
||||
if not new_hash:
|
||||
if not given_hash:
|
||||
await self.plugin_parent._i.update(
|
||||
client, IMPORT_NAME, "avatar", None, entity_jid)
|
||||
# the avatar has been removed, no need to go further
|
||||
return
|
||||
|
||||
avatar_cache = self.host.common_cache.getMetadata(new_hash)
|
||||
avatar_cache = self.host.common_cache.getMetadata(given_hash)
|
||||
if avatar_cache is not None:
|
||||
log.debug(
|
||||
f"New avatar found for [{entity_jid}], it's already in cache, we use it"
|
||||
|
|
@ -516,7 +500,7 @@ class XEP_0054_handler(XMPPHandler):
|
|||
'path': avatar_cache['path'],
|
||||
'filename': avatar_cache['filename'],
|
||||
'media_type': avatar_cache['mime_type'],
|
||||
'cache_uid': new_hash,
|
||||
'cache_uid': given_hash,
|
||||
},
|
||||
entity_jid
|
||||
)
|
||||
|
|
@ -528,7 +512,14 @@ class XEP_0054_handler(XMPPHandler):
|
|||
if vcard is None:
|
||||
log.warning(f"Unexpected empty vCard for {entity_jid}")
|
||||
return
|
||||
await self._checkAvatarHash(client, entity_jid, new_hash)
|
||||
computed_hash = client._xep_0054_avatar_hashes[entity_jid.full()]
|
||||
if computed_hash != given_hash:
|
||||
log.warning(
|
||||
"computed hash differs from given hash for {entity}:\n"
|
||||
"computed: {computed}\ngiven: {given}".format(
|
||||
entity=entity_jid, computed=computed_hash, given=given_hash
|
||||
)
|
||||
)
|
||||
|
||||
def _update(self, presence):
|
||||
defer.ensureDeferred(self.update(presence))
|
||||
|
|
|
|||
|
|
@ -73,7 +73,10 @@ class XEP_0264(object):
|
|||
SIZE_MEDIUM = (640, 640)
|
||||
SIZE_BIG = (1280, 1280)
|
||||
SIZE_FULL_SCREEN = (2560, 2560)
|
||||
SIZES = (SIZE_SMALL, SIZE_MEDIUM, SIZE_FULL_SCREEN)
|
||||
# FIXME: SIZE_FULL_SCREEN is currently discarded as the resulting files are too big
|
||||
# for BoB
|
||||
# TODO: use an other mechanism than BoB for bigger files
|
||||
SIZES = (SIZE_SMALL, SIZE_MEDIUM, SIZE_BIG)
|
||||
|
||||
def __init__(self, host):
|
||||
log.info(_("Plugin XEP_0264 initialization"))
|
||||
|
|
|
|||
|
|
@ -1428,13 +1428,12 @@ class OMEMO:
|
|||
header_elt = encrypted_elt.addElement('header')
|
||||
header_elt['sid'] = str(encryption_data['sid'])
|
||||
|
||||
for to_jid in to_jids:
|
||||
bare_jid_s = to_jid.userhost()
|
||||
|
||||
for rid, data in encryption_data['keys'][bare_jid_s].items():
|
||||
for key_data in encryption_data['keys'].values():
|
||||
for rid, data in key_data.items():
|
||||
key_elt = header_elt.addElement(
|
||||
'key',
|
||||
content=b64enc(data['data']))
|
||||
content=b64enc(data['data'])
|
||||
)
|
||||
key_elt['rid'] = str(rid)
|
||||
if data['pre_key']:
|
||||
key_elt['prekey'] = 'true'
|
||||
|
|
|
|||
Loading…
Reference in a new issue