merge bookmark @

This commit is contained in:
Goffi 2021-11-12 17:21:24 +01:00
commit 14d00a9dda
7 changed files with 327 additions and 201 deletions

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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:

View file

@ -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))

View file

@ -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"))

View file

@ -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'