mirror of
https://gitverse.ru/kpa39l/xmpp-server-prosody.git
synced 2026-09-29 09:45:02 +00:00
137 lines
4.8 KiB
Markdown
137 lines
4.8 KiB
Markdown
# 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). |