comparison src/plugins/plugin_xep_0095.py @ 1577:d04d7402b8e9

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
author Goffi <goffi@goffi.org>
date Wed, 11 Nov 2015 18:19:49 +0100
parents 3265a2639182
children d17772b0fe22
comparison
equal deleted inserted replaced
1576:d5f59ba166fe 1577:d04d7402b8e9
19 19
20 from sat.core.i18n import _ 20 from sat.core.i18n import _
21 from sat.core.constants import Const as C 21 from sat.core.constants import Const as C
22 from sat.core.log import getLogger 22 from sat.core.log import getLogger
23 log = getLogger(__name__) 23 log = getLogger(__name__)
24 from twisted.words.xish import domish 24 from sat.core import exceptions
25 from twisted.words.protocols.jabber import client 25 from twisted.words.protocols.jabber import xmlstream
26 from twisted.words.protocols.jabber import error
27 from zope.interface import implements
28 from wokkel import disco
29 from wokkel import iwokkel
26 import uuid 30 import uuid
27 31
28 from zope.interface import implements
29
30 try:
31 from twisted.words.protocols.xmlstream import XMPPHandler
32 except ImportError:
33 from wokkel.subprotocols import XMPPHandler
34
35 from wokkel import disco, iwokkel
36
37 IQ_SET = '/iq[@type="set"]'
38 NS_SI = 'http://jabber.org/protocol/si'
39 SI_REQUEST = IQ_SET + '/si[@xmlns="' + NS_SI + '"]'
40 SI_PROFILE_HEADER = "http://jabber.org/protocol/si/profile/"
41 32
42 PLUGIN_INFO = { 33 PLUGIN_INFO = {
43 "name": "XEP 0095 Plugin", 34 "name": "XEP 0095 Plugin",
44 "import_name": "XEP-0095", 35 "import_name": "XEP-0095",
45 "type": "XEP", 36 "type": "XEP",
46 "protocols": ["XEP-0095"], 37 "protocols": ["XEP-0095"],
47 "main": "XEP_0095", 38 "main": "XEP_0095",
48 "handler": "yes", 39 "handler": "yes",
49 "description": _("""Implementation of Stream Initiation""") 40 "description": _("""Implementation of Stream Initiation""")
50 } 41 }
42
43
44 IQ_SET = '/iq[@type="set"]'
45 NS_SI = 'http://jabber.org/protocol/si'
46 SI_REQUEST = IQ_SET + '/si[@xmlns="' + NS_SI + '"]'
47 SI_PROFILE_HEADER = "http://jabber.org/protocol/si/profile/"
48 SI_ERROR_CONDITIONS = ('bad-profile', 'no-valid-streams')
51 49
52 50
53 class XEP_0095(object): 51 class XEP_0095(object):
54 52
55 def __init__(self, host): 53 def __init__(self, host):
60 def getHandler(self, profile): 58 def getHandler(self, profile):
61 return XEP_0095_handler(self) 59 return XEP_0095_handler(self)
62 60
63 def registerSIProfile(self, si_profile, callback): 61 def registerSIProfile(self, si_profile, callback):
64 """Add a callback for a SI Profile 62 """Add a callback for a SI Profile
65 param si_profile: SI profile name (e.g. file-transfer) 63
66 param callback: method to call when the profile name is asked""" 64 @param si_profile(unicode): SI profile name (e.g. file-transfer)
65 @param callback(callable): method to call when the profile name is asked
66 """
67 self.si_profiles[si_profile] = callback 67 self.si_profiles[si_profile] = callback
68 68
69 def streamInit(self, iq_el, profile): 69 def unregisterSIProfile(self, si_profile):
70 try:
71 del self.si_profiles[si_profile]
72 except KeyError:
73 log.error(u"Trying to unregister SI profile [{}] which was not registered".format(si_profile))
74
75 def streamInit(self, iq_elt, profile):
70 """This method is called on stream initiation (XEP-0095 #3.2) 76 """This method is called on stream initiation (XEP-0095 #3.2)
71 @param iq_el: IQ element 77
78 @param iq_elt: IQ element
72 @param profile: %(doc_profile)s""" 79 @param profile: %(doc_profile)s"""
73 log.info(_("XEP-0095 Stream initiation")) 80 log.info(_("XEP-0095 Stream initiation"))
74 iq_el.handled = True 81 iq_elt.handled = True
75 si_el = iq_el.firstChildElement() 82 si_elt = iq_elt.elements(NS_SI, 'si').next()
76 si_id = si_el.getAttribute('id') 83 si_id = si_elt['id']
77 si_mime_type = iq_el.getAttribute('mime-type', 'application/octet-stream') 84 si_mime_type = iq_elt.getAttribute('mime-type', 'application/octet-stream')
78 si_profile = si_el.getAttribute('profile') 85 si_profile = si_elt['profile']
79 si_profile_key = si_profile[len(SI_PROFILE_HEADER):] if si_profile.startswith(SI_PROFILE_HEADER) else si_profile 86 si_profile_key = si_profile[len(SI_PROFILE_HEADER):] if si_profile.startswith(SI_PROFILE_HEADER) else si_profile
80 if si_profile_key in self.si_profiles: 87 if si_profile_key in self.si_profiles:
81 #We know this SI profile, we call the callback 88 #We know this SI profile, we call the callback
82 self.si_profiles[si_profile_key](iq_el['id'], iq_el['from'], si_id, si_mime_type, si_el, profile) 89 self.si_profiles[si_profile_key](iq_elt, si_id, si_mime_type, si_elt, profile)
83 else: 90 else:
84 #We don't know this profile, we send an error 91 #We don't know this profile, we send an error
85 self.sendBadProfileError(iq_el['id'], iq_el['from'], profile) 92 self.sendError(iq_elt, 'bad-profile', profile)
86 93
87 def sendRejectedError(self, iq_id, to_jid, reason='Offer Declined', profile=C.PROF_KEY_NONE): 94 def sendError(self, request, condition, profile):
88 """Helper method to send when the stream is rejected 95 """Send IQ error as a result
89 @param iq_id: IQ id
90 @param to_jid: recipient
91 @param reason: human readable reason (string)
92 @param profile: %(doc_profile)s"""
93 self.sendError(iq_id, to_jid, 403, 'cancel', {'text': reason}, profile=profile)
94 96
95 def sendBadProfileError(self, iq_id, to_jid, profile): 97 @param request(domish.Element): original IQ request
96 """Helper method to send when we don't know the SI profile 98 @param condition(str): error condition
97 @param iq_id: IQ id
98 @param to_jid: recipient
99 @param profile: %(doc_profile)s"""
100 self.sendError(iq_id, to_jid, 400, 'modify', profile=profile)
101
102 def sendBadRequestError(self, iq_id, to_jid, profile):
103 """Helper method to send when we don't know the SI profile
104 @param iq_id: IQ id
105 @param to_jid: recipient
106 @param profile: %(doc_profile)s"""
107 self.sendError(iq_id, to_jid, 400, 'cancel', profile=profile)
108
109 def sendFailedError(self, iq_id, to_jid, profile):
110 """Helper method to send when we transfer failed
111 @param iq_id: IQ id
112 @param to_jid: recipient
113 @param profile: %(doc_profile)s"""
114 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)
115
116 def sendError(self, iq_id, to_jid, err_code, err_type='cancel', data={}, profile=C.PROF_KEY_NONE):
117 """Send IQ error as a result
118 @param iq_id: IQ id
119 @param to_jid: recipient
120 @param err_code: error err_code (see XEP-0095 #4.2)
121 @param err_type: one of cancel, modify
122 @param data: error specific data (dictionary)
123 @param profile: %(doc_profile)s 99 @param profile: %(doc_profile)s
124 """ 100 """
125 client_ = self.host.getClient(profile) 101 client = self.host.getClient(profile)
126 result = domish.Element((None, 'iq')) 102 if condition in SI_ERROR_CONDITIONS:
127 result['type'] = 'result' 103 si_condition = condition
128 result['id'] = iq_id 104 condition = 'bad-request'
129 result['to'] = to_jid 105 else:
130 error_el = result.addElement('error') 106 si_condition = None
131 error_el['err_code'] = str(err_code)
132 error_el['type'] = err_type
133 if err_code == 400 and err_type == 'cancel':
134 error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas', 'bad-request'))
135 error_el.addElement((NS_SI, 'no-valid-streams'))
136 elif err_code == 400 and err_type == 'modify':
137 error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas', 'bad-request'))
138 error_el.addElement((NS_SI, 'bad-profile'))
139 elif err_code == 403 and err_type == 'cancel':
140 error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas', 'forbidden'))
141 if 'text' in data:
142 error_el.addElement(('urn:ietf:params:xml:ns:xmpp-stanzas', 'text'), content=data['text'])
143 elif err_code == 500 and err_type == 'cancel':
144 condition_el = error_el.addElement((NS_SI, 'undefined-condition'))
145 if 'custom' in data and data['custom'] == 'failed':
146 condition_el.addContent('Stream failed')
147 107
148 client_.xmlstream.send(result) 108 iq_error_elt = error.StanzaError(condition).toResponse(request)
109 if si_condition is not None:
110 iq_error_elt.error.addElement((NS_SI, si_condition))
149 111
150 def acceptStream(self, iq_id, to_jid, feature_elt, misc_elts=[], profile=C.PROF_KEY_NONE): 112 client.xmlstream.send(iq_error_elt)
113
114 def acceptStream(self, iq_elt, feature_elt, misc_elts=None, profile=C.PROF_KEY_NONE):
151 """Send the accept stream initiation answer 115 """Send the accept stream initiation answer
152 @param iq_id: IQ id 116
153 @param feature_elt: domish element 'feature' containing stream method to use 117 @param iq_elt(domish.Element): initial SI request
154 @param misc_elts: list of domish element to add 118 @param feature_elt(domish.Element): 'feature' element containing stream method to use
155 @param profile: %(doc_profile)s""" 119 @param misc_elts(list[domish.Element]): list of elements to add
156 _client = self.host.getClient(profile) 120 @param profile: %(doc_profile)s
157 assert(_client) 121 """
158 log.info(_("sending stream initiation accept answer")) 122 log.info(_("sending stream initiation accept answer"))
159 result = domish.Element((None, 'iq')) 123 if misc_elts is None:
160 result['type'] = 'result' 124 misc_elts = []
161 result['id'] = iq_id 125 client = self.host.getClient(profile)
162 result['to'] = to_jid 126 result_elt = xmlstream.toResponse(iq_elt, 'result')
163 si = result.addElement('si', NS_SI) 127 si_elt = result_elt.addElement((NS_SI, 'si'))
164 si.addChild(feature_elt) 128 si_elt.addChild(feature_elt)
165 for elt in misc_elts: 129 for elt in misc_elts:
166 si.addChild(elt) 130 si_elt.addChild(elt)
167 _client.xmlstream.send(result) 131 client.xmlstream.send(result_elt)
168 132
169 def proposeStream(self, to_jid, si_profile, feature_elt, misc_elts, mime_type='application/octet-stream', profile_key=C.PROF_KEY_NONE): 133 def _parseOfferResult(self, iq_elt):
134 try:
135 si_elt = iq_elt.elements(NS_SI, "si").next()
136 except StopIteration:
137 log.warning(u"No <si/> element found in result while expected")
138 raise exceptions.DataError
139 return (iq_elt, si_elt)
140
141
142 def proposeStream(self, to_jid, si_profile, feature_elt, misc_elts, mime_type='application/octet-stream', profile=C.PROF_KEY_NONE):
170 """Propose a stream initiation 143 """Propose a stream initiation
171 @param to_jid: recipient (JID) 144
172 @param si_profile: Stream initiation profile (XEP-0095) 145 @param to_jid(jid.JID): recipient
173 @param feature_elt: feature domish element, according to XEP-0020 146 @param si_profile(unicode): Stream initiation profile (XEP-0095)
174 @param misc_elts: list of domish element to add for this profile 147 @param feature_elt(domish.Element): feature element, according to XEP-0020
175 @param mime_type: stream mime type 148 @param misc_elts(list[domish.Element]): list of elements to add
149 @param mime_type(unicode): stream mime type
176 @param profile: %(doc_profile)s 150 @param profile: %(doc_profile)s
177 @return: session id, offer""" 151 @return (tuple): tuple with:
178 current_jid, xmlstream = self.host.getJidNStream(profile_key) 152 - session id (unicode)
179 if not xmlstream: 153 - (D(domish_elt, domish_elt): offer deferred which returl a tuple
180 log.error(_('Asking for an non-existant or not connected profile')) 154 with iq_elt and si_elt
181 return "" 155 """
182 156 client = self.host.getClient(profile)
183 offer = client.IQ(xmlstream, 'set') 157 offer = client.IQ()
184 sid = str(uuid.uuid4()) 158 sid = str(uuid.uuid4())
185 log.debug(_(u"Stream Session ID: %s") % offer["id"]) 159 log.debug(_(u"Stream Session ID: %s") % offer["id"])
186 160
187 offer["from"] = current_jid.full() 161 offer["from"] = client.jid.full()
188 offer["to"] = to_jid.full() 162 offer["to"] = to_jid.full()
189 si = offer.addElement('si', NS_SI) 163 si = offer.addElement('si', NS_SI)
190 si['id'] = sid 164 si['id'] = sid
191 si["mime-type"] = mime_type 165 si["mime-type"] = mime_type
192 si["profile"] = si_profile 166 si["profile"] = si_profile
193 for elt in misc_elts: 167 for elt in misc_elts:
194 si.addChild(elt) 168 si.addChild(elt)
195 si.addChild(feature_elt) 169 si.addChild(feature_elt)
196 170
197 offer.send() 171 offer_d = offer.send()
198 return sid, offer 172 offer_d.addCallback(self._parseOfferResult)
173 return sid, offer_d
199 174
200 175
201 class XEP_0095_handler(XMPPHandler): 176 class XEP_0095_handler(xmlstream.XMPPHandler):
202 implements(iwokkel.IDisco) 177 implements(iwokkel.IDisco)
203 178
204 def __init__(self, plugin_parent): 179 def __init__(self, plugin_parent):
205 self.plugin_parent = plugin_parent 180 self.plugin_parent = plugin_parent
206 self.host = plugin_parent.host 181 self.host = plugin_parent.host
207 182
208 def connectionInitialized(self): 183 def connectionInitialized(self):
209 self.xmlstream.addObserver(SI_REQUEST, self.plugin_parent.streamInit, profile=self.parent.profile) 184 self.xmlstream.addObserver(SI_REQUEST, self.plugin_parent.streamInit, profile=self.parent.profile)
210 185
211 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): 186 def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
212 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] 187 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]
213 188
214 def getDiscoItems(self, requestor, target, nodeIdentifier=''): 189 def getDiscoItems(self, requestor, target, nodeIdentifier=''):
215 return [] 190 return []