Código para limpiar peticiones de suscripción de presencia espurias en el servidor XMPP Isode

En Cleaning spurious presence subscription requests in Isode XMPP server describo una situación en la que el servidor XMPP Isode no limpia las peticiones de subscripción de presencia que vamos denegando. stpeter sugiere una solución. Casi cualquier cliente XMPP permitirá enviar stanzas XMPP de forma manual, pero durante el proceso de investigación del problema escribí bastante código.

A continuación listo y explico el código que utilicé para limpiar las peticiones de subscripción de presencia pendientes en un servidor Isode. El código se programó de forma iterativa, rápida y chapucera, lo justo para salir del paso:

xmpp-20190420.py (Código fuente)

#!/usr/bin/env python3

# This code is in the Public Domain.
# You can do with it whatever you want.
#
# 2018 jcea@jcea.es - https://www.jcea.es/


import socket
import ssl
import base64

import dns.resolver

jid = 'TU JID'
clave = 'TU CLAVE'

usuario, dominio = jid.split('@')
dns = dns.resolver.query(f'_xmpp-client._tcp.{dominio}', 'SRV')
srv = min(dns.response.answer[0].items, key=lambda x: x.priority)

s = socket.socket()
s.connect((srv.target.to_text(), srv.port))

s.send(f"""<stream:stream
from='{jid}'
to='{dominio}'
version='1.0'
xml:lang='en'
xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'>
""".encode('utf-8'))
print(s.recv(9999))

s.send(b"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>")
print(s.recv(9999))

ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
ssl_context.verify_mode = ssl.CERT_REQUIRED
ssl_context.check_hostname = True
ssl_context.load_default_certs()
s = ssl_context.wrap_socket(s, server_hostname=dominio)

print('\nPasamos a TLS\n')

s.send(f"""<stream:stream
from='{jid}'
to='{dominio}'
version='1.0'
xml:lang='en'
xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'>
""".encode('utf-8'))
print(s.recv(9999))

s.send(b"""
<auth mechanism='PLAIN'
xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>%s</auth>
""" % base64.b64encode(f'{jid}\0{usuario}\0{clave}'.encode('utf-8')))
print(s.recv(9999))

s.send(f"""<stream:stream
from='{jid}'
to='{dominio}'
version='1.0'
xml:lang='en'
xmlns='jabber:client'
xmlns:stream='http://etherx.jabber.org/streams'>
""".encode('utf-8'))
print(s.recv(9999))

s.send(b"""
<iq id='qwdc' type='set'>
<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
<resource>DUMMY</resource>
</bind>
</iq>
""")
print(s.recv(9999))

print("\nPreguntamos por qué extensiones soporta el servidor\n")

s.send(f"""
<iq from='{jid}' to='{dominio}' type='get' id='abcd'>
  <query xmlns='http://jabber.org/protocol/disco#info'/>
</iq>
""".encode('utf-8'))
print(s.recv(9999))

# READ:
# https://blog.jcea.es/posts/20190417-xmpp_isode_subscription_cleaning.html

print("\nEnviamos la purga\n")
s.send(b"""
<iq type='set' id='purge-subscribe'>
  <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
    <purge node='http://isode.com/subscribe'/>
  </pubsub>
</iq>
""")

print(s.recv(9999))

En las líneas 15 y 16 especificamos el identificador de usuario XMPP y la clave de acceso. Con esa información la línea 19 obtiene un listado de servidores XMPP y puertos que atienden ese identificador y en la línea 20 elegimos el indicado como más prioritario para conectarnos a él.

La conexión en sí se realiza en las líneas 22-23. Las líneas 25-32 nos presentan ante el servidor XMPP. La línea 33 nos mostrará su respuesta. En las líneas 35 y 36 solicitaremos cifrado y veremos la respuesta del servidor.

Activamos el cifrado en las líneas 38-44. Obsérvese que ignoro las respuestas del servidor XMPP y continúo con mis pasos sin mirar atrás. Obviamente una implementación seria tendría que analizar las respuestas del servidor y proceder adecuadamente. Como he dicho, he hecho lo mínimo imprescindible para resolver mi problema, no he escrito código bonito, elegante y reutilizable.

Cuando tenemos la conexión cifrada, volvemos a presentarnos ante el servidor XMPP (líneas 46-54) y nos autenticamos en las líneas 56-59. La línea 60 nos permite ver la respuesta del servidor.

Ya autenticados, volvemos a abrir una secuencia en el servidor (líneas 62-70) y presentamos nuestra sesión en las líneas 72-77. Sería bueno indicar aquí una prioridad baja para no competir con otros clientes XMPP que podamos tener conectados en otras sesiones e, incluso, configurar una sesión sin anuncio de presencia. En mi caso anuncio presencia porque me interesa recibir las peticiones de suscripción de presencia que haya pendientes.

Seguidamente preguntamos qué extensiones soporta el servidor XMPP. Esto es información para mí, curiosidad.

Por último, enviamos la stanza XMPP indicada en Cleaning spurious presence subscription requests in Isode XMPP server (líneas 90-100) y observamos la respuesta del servidor XMPP (linea 102).

Cumplida la misión, terminamos.

Veamos una sesión de prueba (indentación y saltos de línea añadidos manualmente para que sea más claro):

<?xml version='1.0'?>
<stream:stream xmlns='jabber:client'
               xmlns:stream='http://etherx.jabber.org/streams'
               to='jcea@jabber.org'
               from='jabber.org'
               id='XXXXX' version='1.0'>
  <stream:features>
    <starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'>
      <required/>
    </starttls>
    <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
      <mechanism>DIGEST-MD5</mechanism>
    </mechanisms>
  </stream:features>

El servidor responde a nuestra apertura de una sesión hacia él con una apertura de una sesión hacia nosotros. Además nos indica que debemos autenticarnos mediante el algoritmo DIGEST-MD5 y que es obligatorio que la conexión vaya cifrada.

<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>

Hemos pedido cifrado al servidor XMPP y este nos indica que empecemos a cifrar. A partir de aquí el servidor espera tráfico cifrado.

El estándar XMPP nos dice que tras activar el cifrado debemos negociar la sesión XMPP desde cero, ya que el contenido anterior en la conexión no era de fiar. Repetimos el inicio de sesión y el servidor nos responde con su inicio también:

<?xml version='1.0'?>
<stream:stream xmlns='jabber:client'
               xmlns:stream='http://etherx.jabber.org/streams'
               to='jcea@jabber.org'
               from='jabber.org'
               id='XXXXXXXXXXXX' version='1.0'>
  <stream:features>
    <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
      <mechanism>SCRAM-SHA-1-PLUS</mechanism>
      <mechanism>SCRAM-SHA-1</mechanism>
      <mechanism>DIGEST-MD5</mechanism>
      <mechanism>CRAM-MD5</mechanism>
      <mechanism>PLAIN</mechanism>
      <mechanism>LOGIN</mechanism>
    </mechanisms>
  </stream:features>

Ahora que estamos cifrados, el servidor nos vuelve a requerir autenticación. Obsérvese que ahora nos ofrece más opciones, incluyendo PLAIN y LOGIN. Esas autenticaciones nuevas son aceptables ahora porque la conexión va cifrada. Podemos mandarle la clave en claro, tal cual, sin peligro.

Nos autenticamos y el servidor nos da el OK:

<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>

Ahora que tenemos una conexión cifrada y autenticada, volvemos a establecer una sesión con el servidor XMPP y este nos responde con la suya, una vez más:

<?xml version='1.0'?>
<stream:stream xmlns='jabber:client'
               xmlns:stream='http://etherx.jabber.org/streams'
               to='jcea@jabber.org'
               from='jabber.org'
               id='XXXXX' version='1.0'>
  <stream:features>
    <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
      <required/>
    </bind>
    <ver xmlns='urn:xmpp:features:rosterver'>
      <optional/>
    </ver>
    <session xmlns='urn:ietf:params:xml:ns:xmpp-session'>
      <optional/>
    </session>
    <sm xmlns='urn:xmpp:sm:2'>
      <optional/>
    </sm>
  </stream:features>

Aquí el servidor nos ofrece varias características para la sesión, pero nos dice que tenemos que hacer un bind antes de continuar. Esa operación es lo que hace la sesión pública, con un recurso y una prioridad determinados. Usaremos el recurso "DUMMY". Se lo mandamos al servidor y este nos responde aceptándolo. Tras ese momento ya estamos presentes y visibles en la red federada XMPP:

<iq to='jcea@jabber.org/DUMMY' type='result' id='qwdc'>
  <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>
    <jid>jcea@jabber.org/DUMMY</jid>
  </bind>
</iq>

Preguntamos qué extensiones soporta el servidor y este nos dice:

<iq from='jabber.org' to='jcea@jabber.org/DUMMY' type='result' id='abcd'>
  <query xmlns='http://jabber.org/protocol/disco#info'>
    <identity category='server' type='im' name='Isode M-Link 16.3v13'/>
    <identity category='pubsub' type='pep'/>
    <identity category='directory' type='user'/>
    <feature var='jabber:iq:search'/>
    <feature var='jabber:iq:version'/>
    <feature var='urn:xmpp:ping'/>
    <feature var='http://jabber.org/protocol/disco#info'/>
    <feature var='http://jabber.org/protocol/disco#items'/>
    <feature var='http://jabber.org/protocol/commands'/>
    <feature var='vcard-temp'/>
    <feature var='jabber:iq:private'/>
    <feature var='urn:xmpp:blocking'/>
    <feature var='google:queue'/>
    <feature var='jabber:iq:register'/>
  </query>
</iq>

Hay varios detalles y funcionalidades interesantes. Por ejemplo, el servidor XMPP se identifica como un servidor Isode. Vemos también que soporta el protocolo de bloqueo de stanzas XMPP, cosa que utilizaremos en un artículo futuro [1].

Enviamos la petición de purga y el servidor nos responde indicando el éxito de la operación:

<iq from='jcea@jabber.org' to='jcea@jabber.org/DUMMY'
    type='result' id='purge-subscribe'/>

En este momento nuestra cola de peticiones de suscripción de presencia en el servidor XMPP estará vacía. Justo lo que queríamos.

[1] Actualización 20190522: Proporciono código para el bloqueo de direcciones XMPP en Bloqueo de direcciones XMPP con XEP-0191: Blocking Command.