Prosody, Haproxy, Websocket, ConverseJS
Vamos a contar la conexión del excelente cliente web XMPP ConverseJS (https://conversejs.org/) al servidor Prosody (https://prosody.im/) usando WebSockets (https://developer.mozilla.org/es/docs/Web/API/Websockets_API) con Haproxy (http://www.haproxy.org/) de por medio.
Hasta el momento había estado usando ConverseJS con mi Prosody relativamente bien usando el protocolo BOSH, pero en dos de las instalaciones que mantengo había venido notando continuas desconexiones. No se exactamente porqué, pero me da la sensación de que en entornos donde se requiere mucha seguridad los firewalls tienden a dejar caer las conexiones HTTP que utilizan long-polling, que son conexiones que están activas durante mucho tiempo (hay que tener en cuenta que la petición HTTP más típica, pedir una página web, solo está activa del orden de segundos).
¿Qué está ocurriendo realmente cuando un cliente basado en web que usa Javascript quiere conectarse a un servicio que normalmente usaría TCP para un servicio orientado a la conexión? Pues pasa que HTTP no es un protocolo orientado a una conexión full-duplex. HTTP se pensó para un ciclo petición-respuesta en la que el cliente pide algo y el servidor se lo da, pero el servidor nunca inicia por su cuenta envíos hacia el cliente.
A medida que las aplicaciones web se fueron haciendo más exigentes esto constituyó una limitación. Un servidor XMPP necesita constantemente enviar información a los clientes conectados, por su propia iniciativa. Por eso nació BOSH (flujos bidireccionales sobre http síncrono) que es una de las técnicas HTTP de polling, considerada como de las más eficientes (https://xmpp.org/extensions/xep-0124.html). En este tipo de técnicas, el cliente está continuamente mandando peticiones al servidor del estilo: ¿tienes algo para mi?… ¿tienes algo para mi?… ¿tienes algo para mi?.. y el servidor bloquea esas peticiones hasta que finalmente tiene algo para él. Así se emula una conexión bidireccional asíncrona.
WebSockets
De alguna manera lo que se quiso conseguir con WebSockets es recuperar lo que «se había perdido» usando HTTP. HTTP funciona normalmente sobre TCP, un protocolo full-duplex orientado a la conexión. Las aplicaciones XMPP de escritorio (Gajim, Piding) usan TCP. Cuando se quisieron montar clientes web (útiles cuando el firewall no te permite acceder al puerto XMPP) se necesitaba de alguna manera emular lo que hace TCP, pero encima de HTTP. Y ahí apareció WebSocket (https://datatracker.ietf.org/doc/html/rfc6455). Como reza en su RFC, es útil cuando no se quieren usar técnicas que confían en abrir múltiples conexiones HTTP (como Ajax y HTTP long-polling). Con WebSocket solo se tendrá una conexión, un socket, en clara referencia a los sockets tradicionales de Unix.
Por tanto vemos que es más eficiente y (espero) me mantenga mejor las conexiones en el entorno repleto de firewalls donde lo quiero utilizar.
Prosody
Esto lo tengo montado con Prosody 0.11.9, que ya trae un módulo para websocket que es muy fácil de activar https://prosody.im/doc/websocket. Prosody escuchará estas peticiones en su propio servidor HTTP, que normalmente se encuentra en los puertos 5280 y 5281 (https), de forma que es necesario que un proxy que escuche en el 443 le reenvíe las peticiones. En la página de la documentación viene como configurar Nginx y Apache.
Para las aplicaciones web que realizan conexiones mediante Javascript o AJAX hay unas normas de seguridad denominadas Same Origin Policy (SOP) que restringen hacia donde y cómo se pueden realizar esas conexiones secundarias. Si el protocolo, el nombre de dominio o el puerto difieren de la aplicación cargada en el navegador, se rechazará la conexión (https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy).
Pues bien ¡SOP no aplica a WebSockets! Se pasa esas normas por el socket. Así que hay que tomar una precaución adicional en el servidor, en este caso Prosody, que es restringir el dominio desde el cual le van a venir las conexiones WebSocket.
Si tienes ConverseJS accesible en https://unservidor.tld
, hay que configurar esto en Prosody:
cross_domain_websocket = { "https://unservidor.tld" }
De esta manera, cualquier aplicación web que no provenga de tu dominio, que pretenda iniciar una conexión WebSocket con tu servidor, fallará. El navegador no la parará (SOP no aplica) pero Prosody sí.
Haproxy
Es un balanceador de carga que no uso como balanceador, solo para redirigir tráfico hacia mis contenedores o hacia Prosody (https://danielside.nom.es/2019/12/30/mi-cloud-con-haproxy-y-lxc/).
Estas son las reglas de frontend que permiten a Haproxy reconocer las conexiones WebSocket:
acl url_websocket path_beg /xmpp-websocket
acl hdr_conn_upgrade hdr(Connection) -i upgrade
acl hdr_upgrade_ws hdr(Upgrade) -i websocket
Línea que pasará estas conexiones al backend:
use_backend websocket if url_websocket or hdr_conn_upgrade or hdr_upgrade_ws
Y aquí un ejemplo de backend que pasará la conexión a Prosody:
backend websocket
balance source
option http-server-close
option forceclose
server NOMBRE IP:5280 weight 1 maxconn 1024 check
ConverseJS
La pieza que faltan en la llamada de configuración de ConverseJS es la siguiente:
websocket_url: 'wss://SERVIDOR/xmpp-websocket/'
Esta sustituye a la cadena de conexión BOSH. La documentación: https://conversejs.org/docs/html/configuration.html#websocket-url.
La última parte de la URL (/xmpp-websocket/
)es la que ofrece Prosody por defecto en la configuración del módulo correspondiente.
Conclusiones
Por ahora bien. Continuos mensajes en consola «Websocket closed unexpectedly», pero la cosa parece que mantiene bien la sesión. Gajim te notifica las desconexiones de tus contactos, y hasta ahora no he notado que la gente «se caiga» cuando está accediendo por WebSockets.
Perpetrado el 13 de junio de 2021 por una IN (Inteligencia Natural), la mia, con cierto esfuerzo.
Archivado en categoría(s) XMPP
Deja una respuesta