Mercurial > libervia-backend
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 [] |