plugin XEP-0077, XEP-0100: Adapt to component, and modernize:
- Plugin XEP-0077 can now be used with component, allowing to register methods to return registration form, and to (un)register. - Plugin XEP-0077 now advertises its feature in disco. - Plugin XEP-0100 has been modernized a bit: it is one of the older plugin in Libervia, and it has now some type hints, models and async methods. - Plugin XEP-0100's bridge method `gateways_find` now returns a serialised dict with relevant data. Former XMLUI version has been moved to `gateways_find_xmlui`. rel 449
This commit is contained in:
parent
5c056c049b
commit
c0a9fa611a
2 changed files with 469 additions and 168 deletions
|
|
@ -17,34 +17,56 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from twisted.words.protocols.jabber import jid, xmlstream, client, error as jabber_error
|
from typing import Awaitable, Callable, NamedTuple, cast
|
||||||
|
|
||||||
from twisted.internet import defer, reactor, ssl
|
from twisted.internet import defer, reactor, ssl
|
||||||
from wokkel import data_form
|
from twisted.internet.interfaces import IReactorCore
|
||||||
from libervia.backend.core.i18n import _
|
from twisted.python.failure import Failure
|
||||||
from libervia.backend.core.constants import Const as C
|
from twisted.words.protocols.jabber import (
|
||||||
|
client,
|
||||||
|
error as jabber_error,
|
||||||
|
jid,
|
||||||
|
xmlstream as xmlstream_mod,
|
||||||
|
)
|
||||||
|
from twisted.words.xish import domish
|
||||||
|
from wokkel import data_form, disco, iwokkel
|
||||||
|
from zope.interface import implementer
|
||||||
|
|
||||||
from libervia.backend.core import exceptions
|
from libervia.backend.core import exceptions
|
||||||
|
from libervia.backend.core.constants import Const as C
|
||||||
|
from libervia.backend.core.i18n import _
|
||||||
from libervia.backend.core.log import getLogger
|
from libervia.backend.core.log import getLogger
|
||||||
from libervia.backend.core.xmpp import SatXMPPEntity
|
from libervia.backend.core.xmpp import SatXMPPEntity
|
||||||
from libervia.backend.tools import xml_tools
|
from libervia.backend.tools import xml_tools
|
||||||
|
from libervia.backend.tools.utils import as_deferred, ensure_deferred
|
||||||
|
|
||||||
log = getLogger(__name__)
|
log = getLogger(__name__)
|
||||||
|
|
||||||
NS_REG = "jabber:iq:register"
|
NS_IQ_REGISTER = "jabber:iq:register"
|
||||||
|
IQ_REGISTER_REQUEST = f'{C.IQ_GET}/query[@xmlns="{NS_IQ_REGISTER}"]'
|
||||||
|
IQ_SUBMIT_REQUEST = f'{C.IQ_SET}/query[@xmlns="{NS_IQ_REGISTER}"]'
|
||||||
|
|
||||||
PLUGIN_INFO = {
|
PLUGIN_INFO = {
|
||||||
C.PI_NAME: "XEP 0077 Plugin",
|
C.PI_NAME: "In-Band Registration",
|
||||||
C.PI_IMPORT_NAME: "XEP-0077",
|
C.PI_IMPORT_NAME: "XEP-0077",
|
||||||
C.PI_TYPE: "XEP",
|
C.PI_TYPE: "XEP",
|
||||||
|
C.PI_MODES: C.PLUG_MODE_BOTH,
|
||||||
C.PI_PROTOCOLS: ["XEP-0077"],
|
C.PI_PROTOCOLS: ["XEP-0077"],
|
||||||
C.PI_DEPENDENCIES: [],
|
C.PI_DEPENDENCIES: [],
|
||||||
C.PI_MAIN: "XEP_0077",
|
C.PI_MAIN: "XEP_0077",
|
||||||
C.PI_DESCRIPTION: _("""Implementation of in-band registration"""),
|
C.PI_DESCRIPTION: _("""Implementation of in-band registration."""),
|
||||||
|
C.PI_HANDLER: C.BOOL_TRUE,
|
||||||
}
|
}
|
||||||
|
|
||||||
# FIXME: this implementation is incomplete
|
# FIXME: this implementation is incomplete
|
||||||
|
|
||||||
|
|
||||||
class RegisteringAuthenticator(xmlstream.ConnectAuthenticator):
|
class RegistrationHandlers(NamedTuple):
|
||||||
|
form_handler: Callable
|
||||||
|
submit_handler: Callable
|
||||||
|
|
||||||
|
|
||||||
|
class RegisteringAuthenticator(xmlstream_mod.ConnectAuthenticator):
|
||||||
# FIXME: request IQ is not send to check available fields,
|
# FIXME: request IQ is not send to check available fields,
|
||||||
# while XEP recommand to use it
|
# while XEP recommand to use it
|
||||||
# FIXME: doesn't handle data form or oob
|
# FIXME: doesn't handle data form or oob
|
||||||
|
|
@ -52,28 +74,28 @@ class RegisteringAuthenticator(xmlstream.ConnectAuthenticator):
|
||||||
|
|
||||||
def __init__(self, jid_, password, email=None, check_certificate=True):
|
def __init__(self, jid_, password, email=None, check_certificate=True):
|
||||||
log.debug(_("Registration asked for {jid}").format(jid=jid_))
|
log.debug(_("Registration asked for {jid}").format(jid=jid_))
|
||||||
xmlstream.ConnectAuthenticator.__init__(self, jid_.host)
|
xmlstream_mod.ConnectAuthenticator.__init__(self, jid_.host)
|
||||||
self.jid = jid_
|
self.jid = jid_
|
||||||
self.password = password
|
self.password = password
|
||||||
self.email = email
|
self.email = email
|
||||||
self.check_certificate = check_certificate
|
self.check_certificate = check_certificate
|
||||||
self.registered = defer.Deferred()
|
self.registered = defer.Deferred()
|
||||||
|
|
||||||
def associateWithStream(self, xs):
|
def associateWithStream(self, xmlstream):
|
||||||
xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
|
xmlstream_mod.ConnectAuthenticator.associateWithStream(self, xmlstream)
|
||||||
xs.addObserver(xmlstream.STREAM_AUTHD_EVENT, self.register)
|
xmlstream.addObserver(xmlstream_mod.STREAM_AUTHD_EVENT, self.register)
|
||||||
|
|
||||||
xs.initializers = [client.CheckVersionInitializer(xs)]
|
xmlstream.initializers = [client.CheckVersionInitializer(xmlstream)]
|
||||||
if self.check_certificate:
|
if self.check_certificate:
|
||||||
tls_required, configurationForTLS = True, None
|
tls_required, configurationForTLS = True, None
|
||||||
else:
|
else:
|
||||||
tls_required = False
|
tls_required = False
|
||||||
configurationForTLS = ssl.CertificateOptions(trustRoot=None)
|
configurationForTLS = ssl.CertificateOptions(trustRoot=None)
|
||||||
tls_init = xmlstream.TLSInitiatingInitializer(
|
tls_init = xmlstream_mod.TLSInitiatingInitializer(
|
||||||
xs, required=tls_required, configurationForTLS=configurationForTLS
|
xmlstream, required=tls_required, configurationForTLS=configurationForTLS
|
||||||
)
|
)
|
||||||
|
|
||||||
xs.initializers.append(tls_init)
|
xmlstream.initializers.append(tls_init)
|
||||||
|
|
||||||
def register(self, xmlstream):
|
def register(self, xmlstream):
|
||||||
log.debug(
|
log.debug(
|
||||||
|
|
@ -93,21 +115,23 @@ class RegisteringAuthenticator(xmlstream.ConnectAuthenticator):
|
||||||
|
|
||||||
def registration_cb(self, answer):
|
def registration_cb(self, answer):
|
||||||
log.debug(_("Registration answer: {}").format(answer.toXml()))
|
log.debug(_("Registration answer: {}").format(answer.toXml()))
|
||||||
|
assert self.xmlstream is not None
|
||||||
self.xmlstream.sendFooter()
|
self.xmlstream.sendFooter()
|
||||||
|
|
||||||
def registration_eb(self, failure_):
|
def registration_eb(self, failure_):
|
||||||
log.info(_("Registration failure: {}").format(str(failure_.value)))
|
log.info(_("Registration failure: {}").format(str(failure_.value)))
|
||||||
|
assert self.xmlstream is not None
|
||||||
self.xmlstream.sendFooter()
|
self.xmlstream.sendFooter()
|
||||||
raise failure_
|
raise failure_
|
||||||
|
|
||||||
|
|
||||||
class ServerRegister(xmlstream.XmlStreamFactory):
|
class ServerRegister(xmlstream_mod.XmlStreamFactory):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
xmlstream.XmlStreamFactory.__init__(self, *args, **kwargs)
|
xmlstream_mod.XmlStreamFactory.__init__(self, *args, **kwargs)
|
||||||
self.addBootstrap(xmlstream.STREAM_END_EVENT, self._disconnected)
|
self.addBootstrap(xmlstream_mod.STREAM_END_EVENT, self._disconnected)
|
||||||
|
|
||||||
def clientConnectionLost(self, connector, reason):
|
def clientConnectionLost(self, connector, unused_reason):
|
||||||
connector.disconnect()
|
connector.disconnect()
|
||||||
|
|
||||||
def _disconnected(self, reason):
|
def _disconnected(self, reason):
|
||||||
|
|
@ -121,10 +145,12 @@ class ServerRegister(xmlstream.XmlStreamFactory):
|
||||||
self.authenticator.registered.errback(err)
|
self.authenticator.registered.errback(err)
|
||||||
|
|
||||||
|
|
||||||
class XEP_0077(object):
|
class XEP_0077:
|
||||||
|
|
||||||
def __init__(self, host):
|
def __init__(self, host):
|
||||||
log.info(_("Plugin XEP_0077 initialization"))
|
log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization")
|
||||||
self.host = host
|
self.host = host
|
||||||
|
host.register_namespace("iq-register", NS_IQ_REGISTER)
|
||||||
host.bridge.add_method(
|
host.bridge.add_method(
|
||||||
"in_band_register",
|
"in_band_register",
|
||||||
".plugin",
|
".plugin",
|
||||||
|
|
@ -157,10 +183,162 @@ class XEP_0077(object):
|
||||||
method=self._change_password,
|
method=self._change_password,
|
||||||
async_=True,
|
async_=True,
|
||||||
)
|
)
|
||||||
|
self.handlers = set()
|
||||||
|
|
||||||
|
def get_handler(self, client: SatXMPPEntity) -> xmlstream_mod.XMPPHandler:
|
||||||
|
return XEP_0077_handler(self)
|
||||||
|
|
||||||
|
def register_handler(
|
||||||
|
self,
|
||||||
|
form_handler: Callable[
|
||||||
|
[SatXMPPEntity, domish.Element],
|
||||||
|
Awaitable[tuple[bool, data_form.Form] | None]
|
||||||
|
| defer.Deferred[tuple[bool, data_form.Form] | None]
|
||||||
|
| tuple[bool, data_form.Form]
|
||||||
|
| None,
|
||||||
|
],
|
||||||
|
submit_handler: Callable[
|
||||||
|
[SatXMPPEntity, domish.Element, data_form.Form | None],
|
||||||
|
Awaitable[bool | None] | defer.Deferred[bool | None] | bool | None,
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Register a new handler.
|
||||||
|
|
||||||
|
Mostly useful for component which handle In-Band Registration.
|
||||||
|
@param form_handler: method to call on registration request to get a data form.
|
||||||
|
May be async.
|
||||||
|
The handler must return a data_form.Form instance if it can handle the
|
||||||
|
request, otherwise other handlers will be tried until one returns a data form.
|
||||||
|
@param submit_handler: method to call on registration request to submit the
|
||||||
|
handler.
|
||||||
|
In case of "unregister" request, None will be used instead of a
|
||||||
|
data_form.Form.
|
||||||
|
"""
|
||||||
|
self.handlers.add(RegistrationHandlers(form_handler, submit_handler))
|
||||||
|
|
||||||
|
def _on_register_request(self, iq_elt: domish.Element, client: SatXMPPEntity) -> None:
|
||||||
|
defer.ensureDeferred(self._on_request(iq_elt, client, True))
|
||||||
|
|
||||||
|
def _on_submit_request(self, iq_elt: domish.Element, client: SatXMPPEntity) -> None:
|
||||||
|
defer.ensureDeferred(self._on_request(iq_elt, client, False))
|
||||||
|
|
||||||
|
async def _on_request(
|
||||||
|
self, iq_elt: domish.Element, client: SatXMPPEntity, is_register: bool
|
||||||
|
) -> None:
|
||||||
|
"""Handle a register or submit request.
|
||||||
|
|
||||||
|
@param iq_elt: The IQ element of the request.
|
||||||
|
@param client: Client session.
|
||||||
|
@param is_register: Whether this is a register request (True) or a submit request
|
||||||
|
(False).
|
||||||
|
"""
|
||||||
|
iq_elt.handled = True
|
||||||
|
|
||||||
|
# Submit request must have a form with submitted values.
|
||||||
|
if is_register:
|
||||||
|
handler_type = "register"
|
||||||
|
submit_form = None
|
||||||
|
else:
|
||||||
|
handler_type = "submit"
|
||||||
|
remove_elt = next(iq_elt.query.elements(NS_IQ_REGISTER, "remove"), None)
|
||||||
|
if remove_elt is not None:
|
||||||
|
# This is a unregister request.
|
||||||
|
submit_form = None
|
||||||
|
else:
|
||||||
|
submit_form = data_form.findForm(iq_elt.query, NS_IQ_REGISTER)
|
||||||
|
if submit_form is None:
|
||||||
|
log.warning(f"Data form not found, invalid request: {iq_elt.toXml()}")
|
||||||
|
client.send(
|
||||||
|
jabber_error.StanzaError(
|
||||||
|
"bad-request", text="No data form found."
|
||||||
|
).toResponse(iq_elt)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# We look and run relevant handler.
|
||||||
|
for handlers in self.handlers:
|
||||||
|
if is_register:
|
||||||
|
handler = handlers.form_handler
|
||||||
|
handler_call = as_deferred(handler, client, iq_elt)
|
||||||
|
else:
|
||||||
|
handler = handlers.submit_handler
|
||||||
|
handler_call = as_deferred(handler, client, iq_elt, submit_form)
|
||||||
|
try:
|
||||||
|
callback_ret = await handler_call
|
||||||
|
except jabber_error.StanzaError as e:
|
||||||
|
iq_error_elt = e.toResponse(iq_elt)
|
||||||
|
client.send(iq_error_elt)
|
||||||
|
return
|
||||||
|
except exceptions.PasswordError as e:
|
||||||
|
log.warning("Invalid login or password while registering to service.")
|
||||||
|
iq_error_elt = jabber_error.StanzaError(
|
||||||
|
"forbidden", text=str(e)
|
||||||
|
).toResponse(iq_elt)
|
||||||
|
client.send(iq_error_elt)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
log.exception(
|
||||||
|
f"Error while handling {handler_type} request with {handler}, "
|
||||||
|
"ignoring it."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if callback_ret is not None:
|
||||||
|
if is_register:
|
||||||
|
try:
|
||||||
|
registered, registration_form = callback_ret
|
||||||
|
assert isinstance(registered, bool)
|
||||||
|
assert isinstance(registration_form, data_form.Form)
|
||||||
|
except (TypeError, ValueError, AssertionError) as e:
|
||||||
|
log.warning(
|
||||||
|
f"Invalid return value from {handler}, ignoring it: {e}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
# We need to be sure to have the right namespace for the form.
|
||||||
|
registration_form.formNamespace = NS_IQ_REGISTER
|
||||||
|
iq_result_elt = xmlstream_mod.toResponse(iq_elt, "result")
|
||||||
|
query_elt = iq_result_elt.addElement((NS_IQ_REGISTER, "query"))
|
||||||
|
|
||||||
|
if registered:
|
||||||
|
# The requestor is already registered, we indicate it.
|
||||||
|
query_elt.addElement("registered")
|
||||||
|
|
||||||
|
query_elt.addChild(registration_form.toElement())
|
||||||
|
client.send(iq_result_elt)
|
||||||
|
else:
|
||||||
|
if callback_ret is True:
|
||||||
|
if submit_form is None:
|
||||||
|
log.info(f"User {iq_elt['from']} successfully unregistered.")
|
||||||
|
else:
|
||||||
|
log.info(f"User {iq_elt['from']} successfully registered.")
|
||||||
|
else:
|
||||||
|
log.error(
|
||||||
|
f"Unexpected return value from {handler}, was expecting "
|
||||||
|
'"True".'
|
||||||
|
)
|
||||||
|
client.send(
|
||||||
|
jabber_error.StanzaError(
|
||||||
|
"internal-server-error", text="Error in request handler."
|
||||||
|
).toResponse(iq_elt)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
iq_result_elt = xmlstream_mod.toResponse(iq_elt, "result")
|
||||||
|
client.send(iq_result_elt)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
log.warning(
|
||||||
|
f"No handler found for in-band registration {handler_type} request: "
|
||||||
|
f"{iq_elt.toXml()}."
|
||||||
|
)
|
||||||
|
iq_error_elt = jabber_error.StanzaError("service-unavailable").toResponse(
|
||||||
|
iq_elt
|
||||||
|
)
|
||||||
|
client.send(iq_error_elt)
|
||||||
|
return
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_register_iq(xmlstream_, jid_, password, email=None):
|
def build_register_iq(xmlstream, jid_, password, email=None):
|
||||||
iq_elt = xmlstream.IQ(xmlstream_, "set")
|
iq_elt = xmlstream_mod.IQ(xmlstream, "set")
|
||||||
iq_elt["to"] = jid_.host
|
iq_elt["to"] = jid_.host
|
||||||
query_elt = iq_elt.addElement(("jabber:iq:register", "query"))
|
query_elt = iq_elt.addElement(("jabber:iq:register", "query"))
|
||||||
username_elt = query_elt.addElement("username")
|
username_elt = query_elt.addElement("username")
|
||||||
|
|
@ -172,82 +350,116 @@ class XEP_0077(object):
|
||||||
email_elt.addContent(email)
|
email_elt.addContent(email)
|
||||||
return iq_elt
|
return iq_elt
|
||||||
|
|
||||||
def _reg_cb(self, answer, client, post_treat_cb):
|
|
||||||
"""Called after the first get IQ"""
|
|
||||||
try:
|
|
||||||
query_elt = next(answer.elements(NS_REG, "query"))
|
|
||||||
except StopIteration:
|
|
||||||
raise exceptions.DataError("Can't find expected query element")
|
|
||||||
|
|
||||||
try:
|
|
||||||
x_elem = next(query_elt.elements(data_form.NS_X_DATA, "x"))
|
|
||||||
except StopIteration:
|
|
||||||
# XXX: it seems we have an old service which doesn't manage data forms
|
|
||||||
log.warning(_("Can't find data form"))
|
|
||||||
raise exceptions.DataError(
|
|
||||||
_("This gateway can't be managed by SàT, sorry :(")
|
|
||||||
)
|
|
||||||
|
|
||||||
def submit_form(data, profile):
|
|
||||||
form_elt = xml_tools.xmlui_result_to_elt(data)
|
|
||||||
|
|
||||||
iq_elt = client.IQ()
|
|
||||||
iq_elt["id"] = answer["id"]
|
|
||||||
iq_elt["to"] = answer["from"]
|
|
||||||
query_elt = iq_elt.addElement("query", NS_REG)
|
|
||||||
query_elt.addChild(form_elt)
|
|
||||||
d = iq_elt.send()
|
|
||||||
d.addCallback(self._reg_success, client, post_treat_cb)
|
|
||||||
d.addErrback(self._reg_failure, client)
|
|
||||||
return d
|
|
||||||
|
|
||||||
form = data_form.Form.fromElement(x_elem)
|
|
||||||
submit_reg_id = self.host.register_callback(
|
|
||||||
submit_form, with_data=True, one_shot=True
|
|
||||||
)
|
|
||||||
return xml_tools.data_form_2_xmlui(form, submit_reg_id)
|
|
||||||
|
|
||||||
def _reg_eb(self, failure, client):
|
|
||||||
"""Called when something is wrong with registration"""
|
|
||||||
log.info(_("Registration failure: %s") % str(failure.value))
|
|
||||||
raise failure
|
|
||||||
|
|
||||||
def _reg_success(self, answer, client, post_treat_cb):
|
|
||||||
log.debug(_("registration answer: %s") % answer.toXml())
|
|
||||||
if post_treat_cb is not None:
|
|
||||||
post_treat_cb(jid.JID(answer["from"]), client.profile)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _reg_failure(self, failure, client):
|
|
||||||
log.info(_("Registration failure: %s") % str(failure.value))
|
|
||||||
if failure.value.condition == "conflict":
|
|
||||||
raise exceptions.ConflictError(
|
|
||||||
_("Username already exists, please choose an other one")
|
|
||||||
)
|
|
||||||
raise failure
|
|
||||||
|
|
||||||
def _in_band_register(self, to_jid_s, profile_key=C.PROF_KEY_NONE):
|
def _in_band_register(self, to_jid_s, profile_key=C.PROF_KEY_NONE):
|
||||||
return self.in_band_register, jid.JID(to_jid_s, profile_key)
|
client = self.host.get_client(profile_key)
|
||||||
|
return defer.ensureDeferred(self.in_band_register(client, jid.JID(to_jid_s)))
|
||||||
|
|
||||||
def in_band_register(self, to_jid, post_treat_cb=None, profile_key=C.PROF_KEY_NONE):
|
async def in_band_register(
|
||||||
"""register to a service
|
self,
|
||||||
|
client: SatXMPPEntity,
|
||||||
|
to_jid: jid.JID,
|
||||||
|
post_treat_cb: Callable | None = None,
|
||||||
|
) -> xml_tools.XMLUI:
|
||||||
|
"""Register to a service.
|
||||||
|
|
||||||
@param to_jid(jid.JID): jid of the service to register to
|
Send an IQ request to register with the given service and return the registration
|
||||||
|
form as XML UI.
|
||||||
|
|
||||||
|
@param client: client session.
|
||||||
|
@param to_jid: The JID of the service to register to.
|
||||||
|
@param post_treat_cb: A callback function to handle the registration result, if
|
||||||
|
provided.
|
||||||
|
@return: The registration form as XML UI.
|
||||||
"""
|
"""
|
||||||
# FIXME: this post_treat_cb arguments seems wrong, check it
|
# FIXME: this post_treat_cb arguments seems wrong, check it
|
||||||
client = self.host.get_client(profile_key)
|
|
||||||
log.debug(_("Asking registration for {}").format(to_jid.full()))
|
log.debug(_("Asking registration for {}").format(to_jid.full()))
|
||||||
reg_request = client.IQ("get")
|
reg_request = client.IQ("get")
|
||||||
reg_request["from"] = client.jid.full()
|
reg_request["from"] = client.jid.full()
|
||||||
reg_request["to"] = to_jid.full()
|
reg_request["to"] = to_jid.full()
|
||||||
reg_request.addElement("query", NS_REG)
|
reg_request.addElement("query", NS_IQ_REGISTER)
|
||||||
d = reg_request.send(to_jid.full()).addCallbacks(
|
try:
|
||||||
self._reg_cb,
|
iq_result_elt = await reg_request.send(to_jid.full())
|
||||||
self._reg_eb,
|
except Exception as e:
|
||||||
callbackArgs=[client, post_treat_cb],
|
log.warning(_("Registration failure: {}").format(e))
|
||||||
errbackArgs=[client],
|
raise e
|
||||||
)
|
else:
|
||||||
return d
|
try:
|
||||||
|
query_elt = next(iq_result_elt.elements(NS_IQ_REGISTER, "query"))
|
||||||
|
except StopIteration:
|
||||||
|
raise exceptions.DataError(
|
||||||
|
f"Can't find expected query element: {iq_result_elt.toXml()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
x_elem = next(query_elt.elements(data_form.NS_X_DATA, "x"))
|
||||||
|
except StopIteration:
|
||||||
|
# XXX: it seems we have an old service which doesn't manage data forms
|
||||||
|
log.warning("Can't find data form: {iq_result_elt.toXml()}")
|
||||||
|
raise exceptions.DataError(
|
||||||
|
_(
|
||||||
|
"This gateway is outdated and can't be managed by Libervia, "
|
||||||
|
"sorry :("
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
form = data_form.Form.fromElement(x_elem)
|
||||||
|
xml_ui = xml_tools.data_form_2_xmlui(form, "")
|
||||||
|
d = xml_tools.deferred_ui(self.host, xml_ui)
|
||||||
|
d.addCallback(
|
||||||
|
self._on_xml_ui_cb,
|
||||||
|
client,
|
||||||
|
iq_result_elt["id"],
|
||||||
|
iq_result_elt["from"],
|
||||||
|
post_treat_cb,
|
||||||
|
)
|
||||||
|
d.addErrback(self._on_xml_ui_eb)
|
||||||
|
d.addTimeout(600, cast(IReactorCore, reactor))
|
||||||
|
return xml_ui
|
||||||
|
|
||||||
|
@ensure_deferred
|
||||||
|
async def _on_xml_ui_cb(
|
||||||
|
self,
|
||||||
|
data: dict,
|
||||||
|
client: SatXMPPEntity,
|
||||||
|
stanza_id: str,
|
||||||
|
to_: str,
|
||||||
|
post_treat_cb: Callable | None,
|
||||||
|
) -> None:
|
||||||
|
"""Handle the XML UI result of a registration form.
|
||||||
|
|
||||||
|
Process the filled registration form (from frontend) and send it back to complete
|
||||||
|
the registration process.
|
||||||
|
|
||||||
|
@param data: The filled registration form as a XMLUI Form result.
|
||||||
|
@param client: Client session.
|
||||||
|
@param stanza_id: The ID of the IQ stanza of the registration process.
|
||||||
|
@param to_: The JID of the service that sent the registration form.
|
||||||
|
@param post_treat_cb: A callback function to handle the registration result, if
|
||||||
|
provided.
|
||||||
|
"""
|
||||||
|
form_elt = xml_tools.xmlui_result_to_elt(data, NS_IQ_REGISTER)
|
||||||
|
|
||||||
|
iq_elt = client.IQ()
|
||||||
|
iq_elt["id"] = stanza_id
|
||||||
|
iq_elt["to"] = to_
|
||||||
|
query_elt = iq_elt.addElement("query", NS_IQ_REGISTER)
|
||||||
|
query_elt.addChild(form_elt)
|
||||||
|
try:
|
||||||
|
answer = await iq_elt.send()
|
||||||
|
log.debug(_("registration answer: %s") % answer.toXml())
|
||||||
|
if post_treat_cb is not None:
|
||||||
|
post_treat_cb(jid.JID(answer["from"]), client.profile)
|
||||||
|
except jabber_error.StanzaError as e:
|
||||||
|
log.info(_("Registration failure: {}").format(e))
|
||||||
|
if e.condition == "conflict":
|
||||||
|
raise exceptions.ConflictError(
|
||||||
|
_("Username already exists, please choose another one.")
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def _on_xml_ui_eb(self, failure_: Failure) -> None:
|
||||||
|
"""Handle error during handling of registration form by frontend."""
|
||||||
|
log.warning(f"Error while handling registration form to frontend: {failure_}")
|
||||||
|
|
||||||
def _register_new_account(self, jid_, password, email, host, port):
|
def _register_new_account(self, jid_, password, email, host, port):
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
@ -260,15 +472,20 @@ class XEP_0077(object):
|
||||||
return self.register_new_account(jid.JID(jid_), password, **kwargs)
|
return self.register_new_account(jid.JID(jid_), password, **kwargs)
|
||||||
|
|
||||||
def register_new_account(
|
def register_new_account(
|
||||||
self, jid_, password, email=None, host=None, port=C.XMPP_C2S_PORT
|
self,
|
||||||
|
jid_: jid.JID,
|
||||||
|
password: str,
|
||||||
|
email: str | None = None,
|
||||||
|
host: str | None = None,
|
||||||
|
port: int = C.XMPP_C2S_PORT,
|
||||||
):
|
):
|
||||||
"""register a new account on a XMPP server
|
"""Register a new account on a XMPP server.
|
||||||
|
|
||||||
@param jid_(jid.JID): request jid to register
|
@param jid_: request jid to register
|
||||||
@param password(unicode): password of the account
|
@param password: password of the account
|
||||||
@param email(unicode): email of the account
|
@param email: email of the account
|
||||||
@param host(None, unicode): host of the server to register to
|
@param host: host of the server to register to
|
||||||
@param port(int): port of the server to register to
|
@param port: port of the server to register to
|
||||||
"""
|
"""
|
||||||
if host is None:
|
if host is None:
|
||||||
host = self.host.memory.config_get("", "xmpp_domain", "127.0.0.1")
|
host = self.host.memory.config_get("", "xmpp_domain", "127.0.0.1")
|
||||||
|
|
@ -300,7 +517,7 @@ class XEP_0077(object):
|
||||||
return self.unregister(client, jid.JID(to_jid_s))
|
return self.unregister(client, jid.JID(to_jid_s))
|
||||||
|
|
||||||
def unregister(self, client: SatXMPPEntity, to_jid: jid.JID) -> defer.Deferred:
|
def unregister(self, client: SatXMPPEntity, to_jid: jid.JID) -> defer.Deferred:
|
||||||
"""remove registration from a server/service
|
"""Remove registration from a server/service.
|
||||||
|
|
||||||
BEWARE! if you remove registration from profile own server, this will
|
BEWARE! if you remove registration from profile own server, this will
|
||||||
DELETE THE XMPP ACCOUNT WITHOUT WARNING
|
DELETE THE XMPP ACCOUNT WITHOUT WARNING
|
||||||
|
|
@ -310,9 +527,36 @@ class XEP_0077(object):
|
||||||
iq_elt = client.IQ()
|
iq_elt = client.IQ()
|
||||||
if to_jid is not None:
|
if to_jid is not None:
|
||||||
iq_elt["to"] = to_jid.full()
|
iq_elt["to"] = to_jid.full()
|
||||||
query_elt = iq_elt.addElement((NS_REG, "query"))
|
query_elt = iq_elt.addElement((NS_IQ_REGISTER, "query"))
|
||||||
query_elt.addElement("remove")
|
query_elt.addElement("remove")
|
||||||
d = iq_elt.send()
|
d = iq_elt.send()
|
||||||
if not to_jid or to_jid == jid.JID(client.jid.host):
|
if not to_jid or to_jid == jid.JID(client.jid.host):
|
||||||
d.addCallback(lambda __: client.entity_disconnect())
|
d.addCallback(lambda __: client.entity_disconnect())
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
@implementer(iwokkel.IDisco)
|
||||||
|
class XEP_0077_handler(xmlstream_mod.XMPPHandler):
|
||||||
|
|
||||||
|
def __init__(self, plugin_parent: XEP_0077) -> None:
|
||||||
|
self.plugin_parent = plugin_parent
|
||||||
|
|
||||||
|
def connectionInitialized(self):
|
||||||
|
client = cast(SatXMPPEntity, self.parent)
|
||||||
|
if client.is_component:
|
||||||
|
self.xmlstream.addObserver(
|
||||||
|
IQ_REGISTER_REQUEST,
|
||||||
|
self.plugin_parent._on_register_request,
|
||||||
|
client=client,
|
||||||
|
)
|
||||||
|
self.xmlstream.addObserver(
|
||||||
|
IQ_SUBMIT_REQUEST,
|
||||||
|
self.plugin_parent._on_submit_request,
|
||||||
|
client=client,
|
||||||
|
)
|
||||||
|
|
||||||
|
def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
|
||||||
|
return [disco.DiscoFeature(NS_IQ_REGISTER)]
|
||||||
|
|
||||||
|
def getDiscoItems(self, requestor, target, nodeIdentifier=""):
|
||||||
|
return []
|
||||||
|
|
|
||||||
|
|
@ -17,15 +17,23 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from libervia.backend.core.i18n import _, D_
|
from typing import cast
|
||||||
from libervia.backend.core.constants import Const as C
|
from pydantic import BaseModel, Field
|
||||||
|
from twisted.internet import defer, reactor
|
||||||
|
from twisted.words.protocols.jabber import jid
|
||||||
|
from wokkel import disco
|
||||||
|
|
||||||
from libervia.backend.core import exceptions
|
from libervia.backend.core import exceptions
|
||||||
from libervia.backend.tools import xml_tools
|
from libervia.backend.core.constants import Const as C
|
||||||
|
from libervia.backend.core.core_types import SatXMPPEntity
|
||||||
|
from libervia.backend.core.i18n import D_, _
|
||||||
from libervia.backend.core.log import getLogger
|
from libervia.backend.core.log import getLogger
|
||||||
|
from libervia.backend.models.core import DiscoIdentity
|
||||||
|
from libervia.backend.models.types import StrictJIDType
|
||||||
|
from libervia.backend.tools import xml_tools
|
||||||
|
|
||||||
log = getLogger(__name__)
|
log = getLogger(__name__)
|
||||||
from twisted.words.protocols.jabber import jid
|
|
||||||
from twisted.internet import reactor, defer
|
|
||||||
|
|
||||||
PLUGIN_INFO = {
|
PLUGIN_INFO = {
|
||||||
C.PI_NAME: "Gateways Plugin",
|
C.PI_NAME: "Gateways Plugin",
|
||||||
|
|
@ -38,8 +46,12 @@ PLUGIN_INFO = {
|
||||||
}
|
}
|
||||||
|
|
||||||
WARNING_MSG = D_(
|
WARNING_MSG = D_(
|
||||||
"""Be careful ! Gateways allow you to use an external IM (legacy IM), so you can see your contact as XMPP contacts.
|
"Please exercise caution. Gateways facilitate the use of external instant messaging "
|
||||||
But when you do this, all your messages go throught the external legacy IM server, it is a huge privacy issue (i.e.: all your messages throught the gateway can be monitored, recorded, analysed by the external server, most of time a private company)."""
|
"platforms (legacy IM), enabling you to view your contacts as XMPP contacts. "
|
||||||
|
"However, this process routes all messages through the external legacy IM server, "
|
||||||
|
"raising significant privacy concerns. Specifically, it is possible for the external "
|
||||||
|
"server, often operated by a private company, to monitor, record, and analyze all "
|
||||||
|
"messages that traverse the gateway."
|
||||||
)
|
)
|
||||||
|
|
||||||
GATEWAY_TIMEOUT = 10 # time to wait before cancelling a gateway disco info, in seconds
|
GATEWAY_TIMEOUT = 10 # time to wait before cancelling a gateway disco info, in seconds
|
||||||
|
|
@ -54,9 +66,22 @@ TYPE_DESCRIPTIONS = {
|
||||||
"gadu-gadu": D_("Gadu-Gadu"),
|
"gadu-gadu": D_("Gadu-Gadu"),
|
||||||
"aim": D_("AOL Instant Messenger"),
|
"aim": D_("AOL Instant Messenger"),
|
||||||
"msn": D_("Windows Live Messenger"),
|
"msn": D_("Windows Live Messenger"),
|
||||||
|
"smtp": D_("Email"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GatewayData(BaseModel):
|
||||||
|
entity: StrictJIDType
|
||||||
|
identities: list[DiscoIdentity]
|
||||||
|
|
||||||
|
|
||||||
|
class FoundGateways(BaseModel):
|
||||||
|
available: list[GatewayData]
|
||||||
|
unavailable: list[StrictJIDType] = Field(
|
||||||
|
description="Gateways registered but not answering."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class XEP_0100(object):
|
class XEP_0100(object):
|
||||||
def __init__(self, host):
|
def __init__(self, host):
|
||||||
log.info(_("Gateways plugin initialization"))
|
log.info(_("Gateways plugin initialization"))
|
||||||
|
|
@ -69,7 +94,14 @@ class XEP_0100(object):
|
||||||
".plugin",
|
".plugin",
|
||||||
in_sign="ss",
|
in_sign="ss",
|
||||||
out_sign="s",
|
out_sign="s",
|
||||||
method=self._find_gateways,
|
method=self._gateways_find,
|
||||||
|
)
|
||||||
|
host.bridge.add_method(
|
||||||
|
"gateways_find_xmlui",
|
||||||
|
".plugin",
|
||||||
|
in_sign="ss",
|
||||||
|
out_sign="s",
|
||||||
|
method=self._gateways_find_xmlui,
|
||||||
)
|
)
|
||||||
host.bridge.add_method(
|
host.bridge.add_method(
|
||||||
"gateway_register",
|
"gateway_register",
|
||||||
|
|
@ -77,6 +109,7 @@ class XEP_0100(object):
|
||||||
in_sign="ss",
|
in_sign="ss",
|
||||||
out_sign="s",
|
out_sign="s",
|
||||||
method=self._gateway_register,
|
method=self._gateway_register,
|
||||||
|
async_=True,
|
||||||
)
|
)
|
||||||
self.__menu_id = host.register_callback(self._gateways_menu, with_data=True)
|
self.__menu_id = host.register_callback(self._gateways_menu, with_data=True)
|
||||||
self.__selected_id = host.register_callback(
|
self.__selected_id = host.register_callback(
|
||||||
|
|
@ -101,7 +134,7 @@ class XEP_0100(object):
|
||||||
)
|
)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
raise exceptions.DataError(_("Invalid JID"))
|
raise exceptions.DataError(_("Invalid JID"))
|
||||||
d = self.gateways_find(jid_, profile)
|
d = self.gateways_find_raw(jid_, profile)
|
||||||
d.addCallback(self._gateways_result_2_xmlui, jid_)
|
d.addCallback(self._gateways_result_2_xmlui, jid_)
|
||||||
d.addCallback(lambda xmlui: {"xmlui": xmlui.toXml()})
|
d.addCallback(lambda xmlui: {"xmlui": xmlui.toXml()})
|
||||||
return d
|
return d
|
||||||
|
|
@ -158,7 +191,8 @@ class XEP_0100(object):
|
||||||
if category != "gateway":
|
if category != "gateway":
|
||||||
log.error(
|
log.error(
|
||||||
_(
|
_(
|
||||||
'INTERNAL ERROR: identity category should always be "gateway" in _getTypeString, got "%s"'
|
'INTERNAL ERROR: identity category should always be "gateway" in '
|
||||||
|
'_getTypeString, got "%s"'
|
||||||
)
|
)
|
||||||
% category
|
% category
|
||||||
)
|
)
|
||||||
|
|
@ -174,22 +208,83 @@ class XEP_0100(object):
|
||||||
self.host.presence_set(jid_, profile_key=profile)
|
self.host.presence_set(jid_, profile_key=profile)
|
||||||
|
|
||||||
def _gateway_register(self, target_jid_s, profile_key=C.PROF_KEY_NONE):
|
def _gateway_register(self, target_jid_s, profile_key=C.PROF_KEY_NONE):
|
||||||
d = self.gateway_register(jid.JID(target_jid_s), profile_key)
|
client = self.host.get_client(profile_key)
|
||||||
|
d = self.gateway_register(client, jid.JID(target_jid_s))
|
||||||
d.addCallback(lambda xmlui: xmlui.toXml())
|
d.addCallback(lambda xmlui: xmlui.toXml())
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def gateway_register(self, target_jid, profile_key=C.PROF_KEY_NONE):
|
def gateway_register(
|
||||||
|
self, client: SatXMPPEntity, target_jid: jid.JID
|
||||||
|
) -> defer.Deferred:
|
||||||
"""Register gateway using in-band registration, then log-in to gateway"""
|
"""Register gateway using in-band registration, then log-in to gateway"""
|
||||||
profile = self.host.memory.get_profile_name(profile_key)
|
return defer.ensureDeferred(
|
||||||
assert profile
|
self.host.plugins["XEP-0077"].in_band_register(
|
||||||
d = self.host.plugins["XEP-0077"].in_band_register(
|
client, target_jid, self._registration_successful
|
||||||
target_jid, self._registration_successful, profile
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _gateways_find(self, target_jid_s: str, profile_key: str) -> defer.Deferred[str]:
|
||||||
|
client = self.host.get_client(profile_key)
|
||||||
|
target_jid = jid.JID(target_jid_s) if target_jid_s else client.server_jid
|
||||||
|
d = defer.ensureDeferred(self.gateways_find(client, target_jid))
|
||||||
|
d.addCallback(lambda found_gateways: found_gateways.model_dump_json())
|
||||||
|
# The Deferred will actually return a str due to `model_dump_json`, but type
|
||||||
|
# checker doesn't get that.
|
||||||
|
d = cast(defer.Deferred[str], d)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def _infos_received(self, dl_result, items, target, client):
|
async def gateways_find(
|
||||||
"""Find disco infos about entity, to check if it is a gateway"""
|
self, client: SatXMPPEntity, target: jid.JID
|
||||||
|
) -> FoundGateways:
|
||||||
|
"""Find gateways and convert FoundGateways instance."""
|
||||||
|
gateways_data = await self.gateways_find_raw(client, target)
|
||||||
|
available = []
|
||||||
|
unavailable = []
|
||||||
|
for gw_available, data in gateways_data:
|
||||||
|
if gw_available:
|
||||||
|
data = cast(tuple[jid.JID, list[tuple[tuple[str, str], str]]], data)
|
||||||
|
identities = []
|
||||||
|
for (category, type_), name in data[1]:
|
||||||
|
identities.append(
|
||||||
|
DiscoIdentity(name=name, category=category, type=type_)
|
||||||
|
)
|
||||||
|
available.append(GatewayData(entity=data[0], identities=identities))
|
||||||
|
else:
|
||||||
|
disco_item = cast(disco.DiscoItem, data[1])
|
||||||
|
unavailable.append(disco_item.entity)
|
||||||
|
return FoundGateways(available=available, unavailable=unavailable)
|
||||||
|
|
||||||
|
def _gateways_find_xmlui(
|
||||||
|
self, target_jid_s: str, profile_key: str
|
||||||
|
) -> defer.Deferred[str]:
|
||||||
|
target_jid = jid.JID(target_jid_s)
|
||||||
|
client = self.host.get_client(profile_key)
|
||||||
|
d = defer.ensureDeferred(self.gateways_find_raw(client, target_jid))
|
||||||
|
d.addCallback(self._gateways_result_2_xmlui, target_jid)
|
||||||
|
d.addCallback(lambda xmlui: xmlui.toXml())
|
||||||
|
d = cast(defer.Deferred[str], d)
|
||||||
|
return d
|
||||||
|
|
||||||
|
async def gateways_find_raw(self, client: SatXMPPEntity, target: jid.JID) -> list:
|
||||||
|
"""Find gateways in the target JID, using discovery protocol"""
|
||||||
|
log.debug(
|
||||||
|
_("find gateways (target = {target}, profile = {profile})").format(
|
||||||
|
target=target.full(), profile=client.profile
|
||||||
|
)
|
||||||
|
)
|
||||||
|
disco = await client.disco.requestItems(target)
|
||||||
|
if len(disco._items) == 0:
|
||||||
|
log.debug(_("No gateway found"))
|
||||||
|
return []
|
||||||
|
|
||||||
|
defers_ = []
|
||||||
|
for item in disco._items:
|
||||||
|
log.debug(_("item found: {}").format(item.entity))
|
||||||
|
defers_.append(client.disco.requestInfo(item.entity))
|
||||||
|
deferred_list = defer.DeferredList(defers_)
|
||||||
|
dl_result = await deferred_list
|
||||||
|
reactor.callLater(GATEWAY_TIMEOUT, deferred_list.cancel)
|
||||||
|
items = disco._items
|
||||||
ret = []
|
ret = []
|
||||||
for idx, (success, result) in enumerate(dl_result):
|
for idx, (success, result) in enumerate(dl_result):
|
||||||
if not success:
|
if not success:
|
||||||
|
|
@ -209,58 +304,20 @@ class XEP_0100(object):
|
||||||
if identity[0] == "gateway"
|
if identity[0] == "gateway"
|
||||||
]
|
]
|
||||||
if gateways:
|
if gateways:
|
||||||
log.info(
|
log.debug(
|
||||||
_("Found gateway [%(jid)s]: %(identity_name)s")
|
_("Found gateway [{jid}]: {identity_name}").format(
|
||||||
% {
|
jid=entity.full(),
|
||||||
"jid": entity.full(),
|
identity_name=" - ".join(
|
||||||
"identity_name": " - ".join(
|
|
||||||
[gateway[1] for gateway in gateways]
|
[gateway[1] for gateway in gateways]
|
||||||
),
|
),
|
||||||
}
|
)
|
||||||
)
|
)
|
||||||
ret.append((success, (entity, gateways)))
|
ret.append((success, (entity, gateways)))
|
||||||
else:
|
else:
|
||||||
log.info(
|
log.debug(
|
||||||
_("Skipping [%(jid)s] which is not a gateway")
|
_("Skipping [{jid}] which is not a gateway").format(
|
||||||
% {"jid": entity.full()}
|
jid=entity.full()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def _items_received(self, disco, target, client):
|
|
||||||
"""Look for items with disco protocol, and ask infos for each one"""
|
|
||||||
|
|
||||||
if len(disco._items) == 0:
|
|
||||||
log.debug(_("No gateway found"))
|
|
||||||
return []
|
|
||||||
|
|
||||||
_defers = []
|
|
||||||
for item in disco._items:
|
|
||||||
log.debug(_("item found: %s") % item.entity)
|
|
||||||
_defers.append(client.disco.requestInfo(item.entity))
|
|
||||||
dl = defer.DeferredList(_defers)
|
|
||||||
dl.addCallback(
|
|
||||||
self._infos_received, items=disco._items, target=target, client=client
|
|
||||||
)
|
|
||||||
reactor.callLater(GATEWAY_TIMEOUT, dl.cancel)
|
|
||||||
return dl
|
|
||||||
|
|
||||||
def _find_gateways(self, target_jid_s, profile_key):
|
|
||||||
target_jid = jid.JID(target_jid_s)
|
|
||||||
profile = self.host.memory.get_profile_name(profile_key)
|
|
||||||
if not profile:
|
|
||||||
raise exceptions.ProfileUnknownError
|
|
||||||
d = self.gateways_find(target_jid, profile)
|
|
||||||
d.addCallback(self._gateways_result_2_xmlui, target_jid)
|
|
||||||
d.addCallback(lambda xmlui: xmlui.toXml())
|
|
||||||
return d
|
|
||||||
|
|
||||||
def gateways_find(self, target, profile):
|
|
||||||
"""Find gateways in the target JID, using discovery protocol"""
|
|
||||||
client = self.host.get_client(profile)
|
|
||||||
log.debug(
|
|
||||||
_("find gateways (target = %(target)s, profile = %(profile)s)")
|
|
||||||
% {"target": target.full(), "profile": profile}
|
|
||||||
)
|
|
||||||
d = client.disco.requestItems(target)
|
|
||||||
d.addCallback(self._items_received, target=target, client=client)
|
|
||||||
return d
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue