mirror of
https://gitverse.ru/kpa39l/xmpp-server-prosody.git
synced 2026-09-29 09:45:02 +00:00
Initial commit: Hermes skill xmpp-server-prosody
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user