Mercurial > libervia-backend
comparison libervia/backend/plugins/plugin_xep_0077.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_0077.py@524856bd7b19 |
children | 0d7bb4df2343 |
comparison
equal
deleted
inserted
replaced
4070:d10748475025 | 4071:4b842c1fb686 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 | |
4 # SAT plugin for managing xep-0077 | |
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 twisted.words.protocols.jabber import jid, xmlstream, client, error as jabber_error | |
21 from twisted.internet import defer, reactor, ssl | |
22 from wokkel import data_form | |
23 from libervia.backend.core.i18n import _ | |
24 from libervia.backend.core.constants import Const as C | |
25 from libervia.backend.core import exceptions | |
26 from libervia.backend.core.log import getLogger | |
27 from libervia.backend.core.xmpp import SatXMPPEntity | |
28 from libervia.backend.tools import xml_tools | |
29 | |
30 log = getLogger(__name__) | |
31 | |
32 NS_REG = "jabber:iq:register" | |
33 | |
34 PLUGIN_INFO = { | |
35 C.PI_NAME: "XEP 0077 Plugin", | |
36 C.PI_IMPORT_NAME: "XEP-0077", | |
37 C.PI_TYPE: "XEP", | |
38 C.PI_PROTOCOLS: ["XEP-0077"], | |
39 C.PI_DEPENDENCIES: [], | |
40 C.PI_MAIN: "XEP_0077", | |
41 C.PI_DESCRIPTION: _("""Implementation of in-band registration"""), | |
42 } | |
43 | |
44 # FIXME: this implementation is incomplete | |
45 | |
46 | |
47 class RegisteringAuthenticator(xmlstream.ConnectAuthenticator): | |
48 # FIXME: request IQ is not send to check available fields, | |
49 # while XEP recommand to use it | |
50 # FIXME: doesn't handle data form or oob | |
51 namespace = 'jabber:client' | |
52 | |
53 def __init__(self, jid_, password, email=None, check_certificate=True): | |
54 log.debug(_("Registration asked for {jid}").format(jid=jid_)) | |
55 xmlstream.ConnectAuthenticator.__init__(self, jid_.host) | |
56 self.jid = jid_ | |
57 self.password = password | |
58 self.email = email | |
59 self.check_certificate = check_certificate | |
60 self.registered = defer.Deferred() | |
61 | |
62 def associateWithStream(self, xs): | |
63 xmlstream.ConnectAuthenticator.associateWithStream(self, xs) | |
64 xs.addObserver(xmlstream.STREAM_AUTHD_EVENT, self.register) | |
65 | |
66 xs.initializers = [client.CheckVersionInitializer(xs)] | |
67 if self.check_certificate: | |
68 tls_required, configurationForTLS = True, None | |
69 else: | |
70 tls_required = False | |
71 configurationForTLS = ssl.CertificateOptions(trustRoot=None) | |
72 tls_init = xmlstream.TLSInitiatingInitializer( | |
73 xs, required=tls_required, configurationForTLS=configurationForTLS) | |
74 | |
75 xs.initializers.append(tls_init) | |
76 | |
77 def register(self, xmlstream): | |
78 log.debug(_("Stream started with {server}, now registering" | |
79 .format(server=self.jid.host))) | |
80 iq = XEP_0077.build_register_iq(self.xmlstream, self.jid, self.password, self.email) | |
81 d = iq.send(self.jid.host).addCallbacks(self.registration_cb, self.registration_eb) | |
82 d.chainDeferred(self.registered) | |
83 | |
84 def registration_cb(self, answer): | |
85 log.debug(_("Registration answer: {}").format(answer.toXml())) | |
86 self.xmlstream.sendFooter() | |
87 | |
88 def registration_eb(self, failure_): | |
89 log.info(_("Registration failure: {}").format(str(failure_.value))) | |
90 self.xmlstream.sendFooter() | |
91 raise failure_ | |
92 | |
93 | |
94 class ServerRegister(xmlstream.XmlStreamFactory): | |
95 | |
96 def __init__(self, *args, **kwargs): | |
97 xmlstream.XmlStreamFactory.__init__(self, *args, **kwargs) | |
98 self.addBootstrap(xmlstream.STREAM_END_EVENT, self._disconnected) | |
99 | |
100 def clientConnectionLost(self, connector, reason): | |
101 connector.disconnect() | |
102 | |
103 def _disconnected(self, reason): | |
104 if not self.authenticator.registered.called: | |
105 err = jabber_error.StreamError("Server unexpectedly closed the connection") | |
106 try: | |
107 if reason.value.args[0][0][2] == "certificate verify failed": | |
108 err = exceptions.InvalidCertificate() | |
109 except (IndexError, TypeError): | |
110 pass | |
111 self.authenticator.registered.errback(err) | |
112 | |
113 | |
114 class XEP_0077(object): | |
115 def __init__(self, host): | |
116 log.info(_("Plugin XEP_0077 initialization")) | |
117 self.host = host | |
118 host.bridge.add_method( | |
119 "in_band_register", | |
120 ".plugin", | |
121 in_sign="ss", | |
122 out_sign="", | |
123 method=self._in_band_register, | |
124 async_=True, | |
125 ) | |
126 host.bridge.add_method( | |
127 "in_band_account_new", | |
128 ".plugin", | |
129 in_sign="ssssi", | |
130 out_sign="", | |
131 method=self._register_new_account, | |
132 async_=True, | |
133 ) | |
134 host.bridge.add_method( | |
135 "in_band_unregister", | |
136 ".plugin", | |
137 in_sign="ss", | |
138 out_sign="", | |
139 method=self._unregister, | |
140 async_=True, | |
141 ) | |
142 host.bridge.add_method( | |
143 "in_band_password_change", | |
144 ".plugin", | |
145 in_sign="ss", | |
146 out_sign="", | |
147 method=self._change_password, | |
148 async_=True, | |
149 ) | |
150 | |
151 @staticmethod | |
152 def build_register_iq(xmlstream_, jid_, password, email=None): | |
153 iq_elt = xmlstream.IQ(xmlstream_, "set") | |
154 iq_elt["to"] = jid_.host | |
155 query_elt = iq_elt.addElement(("jabber:iq:register", "query")) | |
156 username_elt = query_elt.addElement("username") | |
157 username_elt.addContent(jid_.user) | |
158 password_elt = query_elt.addElement("password") | |
159 password_elt.addContent(password) | |
160 if email is not None: | |
161 email_elt = query_elt.addElement("email") | |
162 email_elt.addContent(email) | |
163 return iq_elt | |
164 | |
165 def _reg_cb(self, answer, client, post_treat_cb): | |
166 """Called after the first get IQ""" | |
167 try: | |
168 query_elt = next(answer.elements(NS_REG, "query")) | |
169 except StopIteration: | |
170 raise exceptions.DataError("Can't find expected query element") | |
171 | |
172 try: | |
173 x_elem = next(query_elt.elements(data_form.NS_X_DATA, "x")) | |
174 except StopIteration: | |
175 # XXX: it seems we have an old service which doesn't manage data forms | |
176 log.warning(_("Can't find data form")) | |
177 raise exceptions.DataError( | |
178 _("This gateway can't be managed by SàT, sorry :(") | |
179 ) | |
180 | |
181 def submit_form(data, profile): | |
182 form_elt = xml_tools.xmlui_result_to_elt(data) | |
183 | |
184 iq_elt = client.IQ() | |
185 iq_elt["id"] = answer["id"] | |
186 iq_elt["to"] = answer["from"] | |
187 query_elt = iq_elt.addElement("query", NS_REG) | |
188 query_elt.addChild(form_elt) | |
189 d = iq_elt.send() | |
190 d.addCallback(self._reg_success, client, post_treat_cb) | |
191 d.addErrback(self._reg_failure, client) | |
192 return d | |
193 | |
194 form = data_form.Form.fromElement(x_elem) | |
195 submit_reg_id = self.host.register_callback( | |
196 submit_form, with_data=True, one_shot=True | |
197 ) | |
198 return xml_tools.data_form_2_xmlui(form, submit_reg_id) | |
199 | |
200 def _reg_eb(self, failure, client): | |
201 """Called when something is wrong with registration""" | |
202 log.info(_("Registration failure: %s") % str(failure.value)) | |
203 raise failure | |
204 | |
205 def _reg_success(self, answer, client, post_treat_cb): | |
206 log.debug(_("registration answer: %s") % answer.toXml()) | |
207 if post_treat_cb is not None: | |
208 post_treat_cb(jid.JID(answer["from"]), client.profile) | |
209 return {} | |
210 | |
211 def _reg_failure(self, failure, client): | |
212 log.info(_("Registration failure: %s") % str(failure.value)) | |
213 if failure.value.condition == "conflict": | |
214 raise exceptions.ConflictError( | |
215 _("Username already exists, please choose an other one") | |
216 ) | |
217 raise failure | |
218 | |
219 def _in_band_register(self, to_jid_s, profile_key=C.PROF_KEY_NONE): | |
220 return self.in_band_register, jid.JID(to_jid_s, profile_key) | |
221 | |
222 def in_band_register(self, to_jid, post_treat_cb=None, profile_key=C.PROF_KEY_NONE): | |
223 """register to a service | |
224 | |
225 @param to_jid(jid.JID): jid of the service to register to | |
226 """ | |
227 # FIXME: this post_treat_cb arguments seems wrong, check it | |
228 client = self.host.get_client(profile_key) | |
229 log.debug(_("Asking registration for {}").format(to_jid.full())) | |
230 reg_request = client.IQ("get") | |
231 reg_request["from"] = client.jid.full() | |
232 reg_request["to"] = to_jid.full() | |
233 reg_request.addElement("query", NS_REG) | |
234 d = reg_request.send(to_jid.full()).addCallbacks( | |
235 self._reg_cb, | |
236 self._reg_eb, | |
237 callbackArgs=[client, post_treat_cb], | |
238 errbackArgs=[client], | |
239 ) | |
240 return d | |
241 | |
242 def _register_new_account(self, jid_, password, email, host, port): | |
243 kwargs = {} | |
244 if email: | |
245 kwargs["email"] = email | |
246 if host: | |
247 kwargs["host"] = host | |
248 if port: | |
249 kwargs["port"] = port | |
250 return self.register_new_account(jid.JID(jid_), password, **kwargs) | |
251 | |
252 def register_new_account( | |
253 self, jid_, password, email=None, host=None, port=C.XMPP_C2S_PORT | |
254 ): | |
255 """register a new account on a XMPP server | |
256 | |
257 @param jid_(jid.JID): request jid to register | |
258 @param password(unicode): password of the account | |
259 @param email(unicode): email of the account | |
260 @param host(None, unicode): host of the server to register to | |
261 @param port(int): port of the server to register to | |
262 """ | |
263 if host is None: | |
264 host = self.host.memory.config_get("", "xmpp_domain", "127.0.0.1") | |
265 check_certificate = host != "127.0.0.1" | |
266 authenticator = RegisteringAuthenticator( | |
267 jid_, password, email, check_certificate=check_certificate) | |
268 registered_d = authenticator.registered | |
269 server_register = ServerRegister(authenticator) | |
270 reactor.connectTCP(host, port, server_register) | |
271 return registered_d | |
272 | |
273 def _change_password(self, new_password, profile_key): | |
274 client = self.host.get_client(profile_key) | |
275 return self.change_password(client, new_password) | |
276 | |
277 def change_password(self, client, new_password): | |
278 iq_elt = self.build_register_iq(client.xmlstream, client.jid, new_password) | |
279 d = iq_elt.send(client.jid.host) | |
280 d.addCallback( | |
281 lambda __: self.host.memory.param_set( | |
282 "Password", new_password, "Connection", profile_key=client.profile | |
283 ) | |
284 ) | |
285 return d | |
286 | |
287 def _unregister(self, to_jid_s, profile_key): | |
288 client = self.host.get_client(profile_key) | |
289 return self.unregister(client, jid.JID(to_jid_s)) | |
290 | |
291 def unregister( | |
292 self, | |
293 client: SatXMPPEntity, | |
294 to_jid: jid.JID | |
295 ) -> defer.Deferred: | |
296 """remove registration from a server/service | |
297 | |
298 BEWARE! if you remove registration from profile own server, this will | |
299 DELETE THE XMPP ACCOUNT WITHOUT WARNING | |
300 @param to_jid: jid of the service or server | |
301 None to delete client's account (DANGEROUS!) | |
302 """ | |
303 iq_elt = client.IQ() | |
304 if to_jid is not None: | |
305 iq_elt["to"] = to_jid.full() | |
306 query_elt = iq_elt.addElement((NS_REG, "query")) | |
307 query_elt.addElement("remove") | |
308 d = iq_elt.send() | |
309 if not to_jid or to_jid == jid.JID(client.jid.host): | |
310 d.addCallback(lambda __: client.entity_disconnect()) | |
311 return d | |
312 |