Initial commit: Hermes skill xmpp-server-prosody

This commit is contained in:
estorozhenko
2026-09-06 13:51:11 +00:00
commit 3da6c8709b
28 changed files with 2016 additions and 0 deletions
@@ -0,0 +1,137 @@
# Web client (Converse.js) behind Caddy + Caddy bind-mount trap
Session detail from adding the web client to the ICQ XMPP project (2026-08-28).
## Converse.js web client architecture
Serve the web client with a tiny nginx container that BOTH serves static Converse.js AND
proxies the WebSocket to Prosody — so Caddy needs exactly ONE upstream (nginx:8081), not two.
`docker-compose.yml` addition (same compose project as prosody, so `prosody` resolves
by container name on the shared network):
```yaml
webchat:
image: nginx:alpine
container_name: icq-webchat
restart: unless-stopped
volumes:
- ./webchat/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./webchat:/usr/share/nginx/html:ro
ports:
- "8081:8081"
depends_on:
- prosody
```
`webchat/nginx.conf`:
```nginx
server {
listen 8081;
server_name chat.nixg.ru;
root /usr/share/nginx/html;
index index.html;
location /xmpp-websocket {
proxy_pass http://prosody:5280/xmpp-websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location / { try_files $uri $uri/ /index.html; }
}
```
`webchat/index.html` — Converse.js from CDN:
```html
<link rel="stylesheet" href="https://cdn.conversejs.org/css/converse.min.css">
<div id="conversejs"></div>
<script src="https://cdn.conversejs.org/dist/converse.min.js"></script>
<script>
converse.initialize({
bosh_service_url: 'wss://chat.nixg.ru/xmpp-websocket',
theme: 'concord',
i18n: 'ru',
view_mode: 'overlayed', // or 'fullscreen'
allow_file_transfer: false, // needs http_upload component
});
</script>
```
Caddy block (point at nginx, NOT directly at Prosody 5280):
```
chat.nixg.ru {
reverse_proxy 10.8.0.2:8081 {
header_up Host {host}
header_up X-Forwarded-Proto https
}
}
```
## mod_websocket handshake requirements (diagnosing 501/403)
- A plain `curl` GET to `/xmpp-websocket` returns a generic "It works! Now point your
WebSocket client to this URL" page — that does NOT prove WebSocket works. You must send
a real WebSocket handshake.
- **`Sec-WebSocket-Protocol: xmpp` is REQUIRED.** Without it Prosody answers
**501 Not Implemented** ("Client didn't want to talk XMPP" — mod_websocket.lua ~line 217).
- With the protocol header but no allowed `Origin`, Prosody answers **403 Forbidden**
(cross-domain check, mod_websocket.lua ~line 225). Fix: GLOBAL config section
(above VirtualHost):
```lua
cross_domain_websocket = { "https://chat.nixg.ru" }
```
then restart prosody. Browser clients ALWAYS send `Origin`, so this must be set.
- curl cannot do a real WS handshake — its 501/400/403 results are expected noise, NOT
proof of breakage. Test with a raw-socket handshake script:
```python
import socket, base64, os
s = socket.create_connection(("127.0.0.1", 8081), timeout=6)
key = base64.b64encode(os.urandom(16)).decode()
req = (f"GET /xmpp-websocket HTTP/1.1\r\nHost: chat.nixg.ru\r\n"
"Upgrade: websocket\r\nConnection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n"
"Sec-WebSocket-Protocol: xmpp\r\nOrigin: https://chat.nixg.ru\r\n\r\n")
s.sendall(req.encode())
resp = b""
while b"\r\n\r\n" not in resp:
resp += s.recv(4096)
print(resp.split(b"\r\n")[0].decode()) # expect HTTP/1.1 101 Switching Protocols
s.close()
```
## Caddy bind-mount inode trap (edit via tee/redirect silently ignored)
Editing `/opt/caddy/Caddyfile` on the host with `sudo tee` or `> redirect` creates a NEW
inode. The running caddy container keeps the OLD inode bound (bind mount), so:
- `docker exec caddy caddy reload --config /etc/caddy/Caddyfile` SUCCEEDS but serves the OLD config.
- `grep` of the file INSIDE the container differs from the file ON the host
(different inode & size via `stat`).
Fix: `docker compose restart caddy` (rebinds the mount to the new inode), then verify by
grepping the file inside the container.
Diagnostic that revealed it:
```bash
sudo stat -c "%i %s %y" /opt/caddy/Caddyfile # host inode
sudo docker exec caddy stat -c "%i %s %y" /etc/caddy/Caddyfile # container inode — differs!
# also: caddy adapt --config ... | grep upstreams shows the OLD dial IP
```
## Security group / provider firewall (Timeweb-style)
- Cloud providers (Timeweb etc.) default-close non-standard ports. Open TCP 5222 + 5269
in the provider's security group for the public VPS. XMPP uses TCP only — no UDP.
- When creating a security group, keep the broad egress rule (`Any/Any/0.0.0.0/0`); if the
group only allows metadata-IP egress, the VPS loses general internet (this was avoided —
the xmpp group was ADDITIONAL to the base group, so egress stayed open).