comparison libervia/backend/plugins/plugin_xep_0095.py @ 4071:4b842c1fb686

refactoring: renamed `sat` package to `libervia.backend`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 11:49:51 +0200
parents sat/plugins/plugin_xep_0095.py@524856bd7b19
children
comparison
equal deleted inserted replaced
4070:d10748475025 4071:4b842c1fb686
1 #!/usr/bin/env python3
2
3
4 # SAT plugin for managing xep-0095
5 # Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from libervia.backend.core.i18n import _
21 from libervia.backend.core.constants import Const as C
22 from libervia.backend.core.log import getLogger
23
24 log = getLogger(__name__)
25 from libervia.backend.core import exceptions
26 from twisted.words.protocols.jabber import xmlstream
27 from twisted.words.protocols.jabber import error
28 from zope.interface import implementer
29 from wokkel import disco
30 from wokkel import iwokkel
31 import uuid
32
33
34 PLUGIN_INFO = {
35 C.PI_NAME: "XEP 0095 Plugin",
36 C.PI_IMPORT_NAME: "XEP-0095",
37 C.PI_TYPE: "XEP",
38 C.PI_PROTOCOLS: ["XEP-0095"],
39 C.PI_MAIN: "XEP_0095",
40 C.PI_HANDLER: "yes",
41 C.PI_DESCRIPTION: _("""Implementation of Stream Initiation"""),
42 }
43
44
45 IQ_SET = '/iq[@type="set"]'
46 NS_SI = "http://jabber.org/protocol/si"
47 SI_REQUEST = IQ_SET + '/si[@xmlns="' + NS_SI + '"]'
48 SI_PROFILE_HEADER = "http://jabber.org/protocol/si/profile/"
49 SI_ERROR_CONDITIONS = ("bad-profile", "no-valid-streams")
50
51
52 class XEP_0095(object):
53 def __init__(self, host):
54 log.info(_("Plugin XEP_0095 initialization"))
55 self.host = host
56 self.si_profiles = {} # key: SI profile, value: callback
57
58 def get_handler(self, client):
59 return XEP_0095_handler(self)
60
61 def register_si_profile(self, si_profile, callback):
62 """Add a callback for a SI Profile
63
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
68
69 def unregister_si_profile(self, si_profile):
70 try:
71 del self.si_profiles[si_profile]
72 except KeyError:
73 log.error(
74 "Trying to unregister SI profile [{}] which was not registered".format(
75 si_profile
76 )
77 )
78
79 def stream_init(self, iq_elt, client):
80 """This method is called on stream initiation (XEP-0095 #3.2)
81
82 @param iq_elt: IQ element
83 """
84 log.info(_("XEP-0095 Stream initiation"))
85 iq_elt.handled = True
86 si_elt = next(iq_elt.elements(NS_SI, "si"))
87 si_id = si_elt["id"]
88 si_mime_type = iq_elt.getAttribute("mime-type", "application/octet-stream")
89 si_profile = si_elt["profile"]
90 si_profile_key = (
91 si_profile[len(SI_PROFILE_HEADER) :]
92 if si_profile.startswith(SI_PROFILE_HEADER)
93 else si_profile
94 )
95 if si_profile_key in self.si_profiles:
96 # We know this SI profile, we call the callback
97 self.si_profiles[si_profile_key](client, iq_elt, si_id, si_mime_type, si_elt)
98 else:
99 # We don't know this profile, we send an error
100 self.sendError(client, iq_elt, "bad-profile")
101
102 def sendError(self, client, request, condition):
103 """Send IQ error as a result
104
105 @param request(domish.Element): original IQ request
106 @param condition(str): error condition
107 """
108 if condition in SI_ERROR_CONDITIONS:
109 si_condition = condition
110 condition = "bad-request"
111 else:
112 si_condition = None
113
114 iq_error_elt = error.StanzaError(condition).toResponse(request)
115 if si_condition is not None:
116 iq_error_elt.error.addElement((NS_SI, si_condition))
117
118 client.send(iq_error_elt)
119
120 def accept_stream(self, client, iq_elt, feature_elt, misc_elts=None):
121 """Send the accept stream initiation answer
122
123 @param iq_elt(domish.Element): initial SI request
124 @param feature_elt(domish.Element): 'feature' element containing stream method to use
125 @param misc_elts(list[domish.Element]): list of elements to add
126 """
127 log.info(_("sending stream initiation accept answer"))
128 if misc_elts is None:
129 misc_elts = []
130 result_elt = xmlstream.toResponse(iq_elt, "result")
131 si_elt = result_elt.addElement((NS_SI, "si"))
132 si_elt.addChild(feature_elt)
133 for elt in misc_elts:
134 si_elt.addChild(elt)
135 client.send(result_elt)
136
137 def _parse_offer_result(self, iq_elt):
138 try:
139 si_elt = next(iq_elt.elements(NS_SI, "si"))
140 except StopIteration:
141 log.warning("No <si/> element found in result while expected")
142 raise exceptions.DataError
143 return (iq_elt, si_elt)
144
145 def propose_stream(
146 self,
147 client,
148 to_jid,
149 si_profile,
150 feature_elt,
151 misc_elts,
152 mime_type="application/octet-stream",
153 ):
154 """Propose a stream initiation
155
156 @param to_jid(jid.JID): recipient
157 @param si_profile(unicode): Stream initiation profile (XEP-0095)
158 @param feature_elt(domish.Element): feature element, according to XEP-0020
159 @param misc_elts(list[domish.Element]): list of elements to add
160 @param mime_type(unicode): stream mime type
161 @return (tuple): tuple with:
162 - session id (unicode)
163 - (D(domish_elt, domish_elt): offer deferred which returl a tuple
164 with iq_elt and si_elt
165 """
166 offer = client.IQ()
167 sid = str(uuid.uuid4())
168 log.debug(_("Stream Session ID: %s") % offer["id"])
169
170 offer["from"] = client.jid.full()
171 offer["to"] = to_jid.full()
172 si = offer.addElement("si", NS_SI)
173 si["id"] = sid
174 si["mime-type"] = mime_type
175 si["profile"] = si_profile
176 for elt in misc_elts:
177 si.addChild(elt)
178 si.addChild(feature_elt)
179
180 offer_d = offer.send()
181 offer_d.addCallback(self._parse_offer_result)
182 return sid, offer_d
183
184
185 @implementer(iwokkel.IDisco)
186 class XEP_0095_handler(xmlstream.XMPPHandler):
187
188 def __init__(self, plugin_parent):
189 self.plugin_parent = plugin_parent
190 self.host = plugin_parent.host
191
192 def connectionInitialized(self):
193 self.xmlstream.addObserver(
194 SI_REQUEST, self.plugin_parent.stream_init, client=self.parent
195 )
196
197 def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
198 return [disco.DiscoFeature(NS_SI)] + [
199 disco.DiscoFeature(
200 "http://jabber.org/protocol/si/profile/{}".format(profile_name)
201 )
202 for profile_name in self.plugin_parent.si_profiles
203 ]
204
205 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
206 return []