plugins XEP-0020, XEP-0065, XEP-0095, XEP-0096: fixed file copy with Stream Initiation:

/!\ range is not working yet
    /!\ pipe plugin is broken for now
This commit is contained in:
Goffi 2015-11-11 18:19:49 +01:00
commit dc0dbb149c
4 changed files with 536 additions and 552 deletions

View file

@ -21,23 +21,14 @@ from sat.core.i18n import _
from sat.core.constants import Const as C
from sat.core.log import getLogger
log = getLogger(__name__)
from twisted.words.xish import domish
from twisted.words.protocols.jabber import client
from sat.core import exceptions
from twisted.words.protocols.jabber import xmlstream
from twisted.words.protocols.jabber import error
from zope.interface import implements
from wokkel import disco
from wokkel import iwokkel
import uuid
from zope.interface import implements
try:
from twisted.words.protocols.xmlstream import XMPPHandler
except ImportError:
from wokkel.subprotocols import XMPPHandler
from wokkel import disco, iwokkel
IQ_SET = '/iq[@type="set"]'
NS_SI = 'http://jabber.org/protocol/si'
SI_REQUEST = IQ_SET + '/si[@xmlns="' + NS_SI + '"]'
SI_PROFILE_HEADER = "http://jabber.org/protocol/si/profile/"
PLUGIN_INFO = {
"name": "XEP 0095 Plugin",
@ -50,6 +41,13 @@ PLUGIN_INFO = {
}
IQ_SET = '/iq[@type="set"]'
NS_SI = 'http://jabber.org/protocol/si'
SI_REQUEST = IQ_SET + '/si[@xmlns="' + NS_SI + '"]'
SI_PROFILE_HEADER = "http://jabber.org/protocol/si/profile/"
SI_ERROR_CONDITIONS = ('bad-profile', 'no-valid-streams')
class XEP_0095(object):
def __init__(self, host):
@ -62,129 +60,105 @@ class XEP_0095(object):
def registerSIProfile(self, si_profile, callback):
"""Add a callback for a SI Profile
param si_profile: SI profile name (e.g. file-transfer)
param callback: method to call when the profile name is asked"""
@param si_profile(unicode): SI profile name (e.g. file-transfer)
@param callback(callable): method to call when the profile name is asked
"""
self.si_profiles[si_profile] = callback
def streamInit(self, iq_el, profile):
def unregisterSIProfile(self, si_profile):
try:
del self.si_profiles[si_profile]
except KeyError:
log.error(u"Trying to unregister SI profile [{}] which was not registered".format(si_profile))
def streamInit(self, iq_elt, profile):
"""This method is called on stream initiation (XEP-0095 #3.2)
@param iq_el: IQ element
@param iq_elt: IQ element
@param profile: %(doc_profile)s"""
log.info(_("XEP-0095 Stream initiation"))
iq_el.handled = True
si_el = iq_el.firstChildElement()
si_id = si_el.getAttribute('id')
si_mime_type = iq_el.getAttribute('mime-type', 'application/octet-stream')
si_profile = si_el.getAttribute('profile')
iq_elt.handled = True
si_elt = iq_elt.elements(NS_SI, 'si').next()
si_id = si_elt['id']
si_mime_type = iq_elt.getAttribute('mime-type', 'application/octet-stream')
si_profile = si_elt['profile']
si_profile_key = si_profile[len(SI_PROFILE_HEADER):] if si_profile.startswith(SI_PROFILE_HEADER) else si_profile
if si_profile_key in self.si_profiles:
#We know this SI profile, we call the callback
self.si_profiles[si_profile_key](iq_el['id'], iq_el['from'], si_id, si_mime_type, si_el, profile)
self.si_profiles[si_profile_key](iq_elt, si_id, si_mime_type, si_elt, profile)
else:
#We don't know this profile, we send an error
self.sendBadProfileError(iq_el['id'], iq_el['from'], profile)
self.sendError(iq_elt, 'bad-profile', profile)
def sendRejectedError(self, iq_id, to_jid, reason='Offer Declined', profile=C.PROF_KEY_NONE):
"""Helper method to send when the stream is rejected
@param iq_id: IQ id
@param to_jid: recipient
@param reason: human readable reason (string)
@param profile: %(doc_profile)s"""
self.sendError(iq_id, to_jid, 403, 'cancel', {'text': reason}, profile=profile)
def sendBadProfileError(self, iq_id, to_jid, profile):
"""Helper method to send when we don't know the SI profile
@param iq_id: IQ id
@param to_jid: recipient
@param profile: %(doc_profile)s"""
self.sendError(iq_id, to_jid, 400, 'modify', profile=profile)
def sendBadRequestError(self, iq_id, to_jid, profile):
"""Helper method to send when we don't know the SI profile
@param iq_id: IQ id
@param to_jid: recipient
@param profile: %(doc_profile)s"""
self.sendError(iq_id, to_jid, 400, 'cancel', profile=profile)
def sendFailedError(self, iq_id, to_jid, profile):
"""Helper method to send when we transfer failed
@param iq_id: IQ id
@param to_jid: recipient
@param profile: %(doc_profile)s"""
self.sendError(iq_id, to_jid, 500, 'cancel', {'custom': 'failed'}, profile=profile) # as there is no lerror code for failed transfer, we use 500 (undefined-condition)
def sendError(self, iq_id, to_jid, err_code, err_type='cancel', data={}, profile=C.PROF_KEY_NONE):
def sendError(self, request, condition, profile):
"""Send IQ error as a result
@param iq_id: IQ id
@param to_jid: recipient
@param err_code: error err_code (see XEP-0095 #4.2)
@param err_type: one of cancel, modify
@param data: error specific data (dictionary)
@param request(domish.Element): original IQ request
@param condition(str): error condition
@param profile: %(doc_profile)s
"""
client_ = self.host.getClient(profile)
result = domish.Element((None, 'iq'))
result['type'] = 'result'
result['id'] = iq_id
result['to'] = to_jid
error_el = result.addElement('error')
error_el['err_code'] = str(err_code)
error_el['type'] = err_type
if err_code == 400 and err_type == 'cancel':
error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas', 'bad-request'))
error_el.addElement((NS_SI, 'no-valid-streams'))
elif err_code == 400 and err_type == 'modify':
error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas', 'bad-request'))
error_el.addElement((NS_SI, 'bad-profile'))
elif err_code == 403 and err_type == 'cancel':
error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas', 'forbidden'))
if 'text' in data:
error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas', 'text'), content=data['text'])
elif err_code == 500 and err_type == 'cancel':
condition_el = error_el.addElement((NS_SI, 'undefined-condition'))
if 'custom' in data and data['custom'] == 'failed':
condition_el.addContent('Stream failed')
client = self.host.getClient(profile)
if condition in SI_ERROR_CONDITIONS:
si_condition = condition
condition = 'bad-request'
else:
si_condition = None
client_.xmlstream.send(result)
iq_error_elt = error.StanzaError(condition).toResponse(request)
if si_condition is not None:
iq_error_elt.error.addElement((NS_SI, si_condition))
def acceptStream(self, iq_id, to_jid, feature_elt, misc_elts=[], profile=C.PROF_KEY_NONE):
client.xmlstream.send(iq_error_elt)
def acceptStream(self, iq_elt, feature_elt, misc_elts=None, profile=C.PROF_KEY_NONE):
"""Send the accept stream initiation answer
@param iq_id: IQ id
@param feature_elt: domish element 'feature' containing stream method to use
@param misc_elts: list of domish element to add
@param profile: %(doc_profile)s"""
_client = self.host.getClient(profile)
assert(_client)
log.info(_("sending stream initiation accept answer"))
result = domish.Element((None, 'iq'))
result['type'] = 'result'
result['id'] = iq_id
result['to'] = to_jid
si = result.addElement('si', NS_SI)
si.addChild(feature_elt)
for elt in misc_elts:
si.addChild(elt)
_client.xmlstream.send(result)
def proposeStream(self, to_jid, si_profile, feature_elt, misc_elts, mime_type='application/octet-stream', profile_key=C.PROF_KEY_NONE):
"""Propose a stream initiation
@param to_jid: recipient (JID)
@param si_profile: Stream initiation profile (XEP-0095)
@param feature_elt: feature domish element, according to XEP-0020
@param misc_elts: list of domish element to add for this profile
@param mime_type: stream mime type
@param iq_elt(domish.Element): initial SI request
@param feature_elt(domish.Element): 'feature' element containing stream method to use
@param misc_elts(list[domish.Element]): list of elements to add
@param profile: %(doc_profile)s
@return: session id, offer"""
current_jid, xmlstream = self.host.getJidNStream(profile_key)
if not xmlstream:
log.error(_('Asking for an non-existant or not connected profile'))
return ""
"""
log.info(_("sending stream initiation accept answer"))
if misc_elts is None:
misc_elts = []
client = self.host.getClient(profile)
result_elt = xmlstream.toResponse(iq_elt, 'result')
si_elt = result_elt.addElement((NS_SI, 'si'))
si_elt.addChild(feature_elt)
for elt in misc_elts:
si_elt.addChild(elt)
client.xmlstream.send(result_elt)
offer = client.IQ(xmlstream, 'set')
def _parseOfferResult(self, iq_elt):
try:
si_elt = iq_elt.elements(NS_SI, "si").next()
except StopIteration:
log.warning(u"No <si/> element found in result while expected")
raise exceptions.DataError
return (iq_elt, si_elt)
def proposeStream(self, to_jid, si_profile, feature_elt, misc_elts, mime_type='application/octet-stream', profile=C.PROF_KEY_NONE):
"""Propose a stream initiation
@param to_jid(jid.JID): recipient
@param si_profile(unicode): Stream initiation profile (XEP-0095)
@param feature_elt(domish.Element): feature element, according to XEP-0020
@param misc_elts(list[domish.Element]): list of elements to add
@param mime_type(unicode): stream mime type
@param profile: %(doc_profile)s
@return (tuple): tuple with:
- session id (unicode)
- (D(domish_elt, domish_elt): offer deferred which returl a tuple
with iq_elt and si_elt
"""
client = self.host.getClient(profile)
offer = client.IQ()
sid = str(uuid.uuid4())
log.debug(_(u"Stream Session ID: %s") % offer["id"])
offer["from"] = current_jid.full()
offer["from"] = client.jid.full()
offer["to"] = to_jid.full()
si = offer.addElement('si', NS_SI)
si['id'] = sid
@ -194,11 +168,12 @@ class XEP_0095(object):
si.addChild(elt)
si.addChild(feature_elt)
offer.send()
return sid, offer
offer_d = offer.send()
offer_d.addCallback(self._parseOfferResult)
return sid, offer_d
class XEP_0095_handler(XMPPHandler):
class XEP_0095_handler(xmlstream.XMPPHandler):
implements(iwokkel.IDisco)
def __init__(self, plugin_parent):
@ -209,7 +184,7 @@ class XEP_0095_handler(XMPPHandler):
self.xmlstream.addObserver(SI_REQUEST, self.plugin_parent.streamInit, profile=self.parent.profile)
def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
return [disco.DiscoFeature(NS_SI)] + [disco.DiscoFeature("http://jabber.org/protocol/si/profile/%s" % profile_name) for profile_name in self.plugin_parent.si_profiles]
return [disco.DiscoFeature(NS_SI)] + [disco.DiscoFeature(u"http://jabber.org/protocol/si/profile/{}".format(profile_name)) for profile_name in self.plugin_parent.si_profiles]
def getDiscoItems(self, requestor, target, nodeIdentifier=''):
return []