Per https://datatracker.ietf.org/doc/html/rfc8058,
we send "announcement" mails with
List-Unsubscribe: <${apiUrl}/unsubscribe/${userId}/${oneClickUnsubscribeToken}>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
and handle
POST /api/unsubscribe/:user/:token => this unsubscribes
GET /api/unsubscribe/:user/:token => 302 /unsubscribe/:user/:token
GET /unsubscribe/:user/:token => user-visible page with clickthrough confirmation
In this configuration, compatible MUAs will show an "unsubscribe" button
that, when clicked, will POST to the URL directly
Less-compatible MUAs (and scanners) will open the page directly
which will redirect to a click-though; interactive users will be able to
unsubscribe, scanners won't unsubscribe by accident
Nothing /actually/ sends non-reactive mails,
so this is never used at this time
Closes#854
$ nc -C tarta.nabijaczleweli.xyz 12122
GET /streaming?_t=1752534314122 HTTP/1.1
Host: tarta.nabijaczleweli.xyz:12122
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0
Accept: */*
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: AAAAAAAAAAAAAAAAAAAAAA==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
the parameters almost don't matter so long as the server
replies with an upgrade, then press enter once or twice,
at which point the server will have crashed after
INFO 1 [core nest] NestFactory: Starting Nest application...
ERR * [core] Uncaught exception (uncaughtException): RangeError: Invalid WebSocket frame: invalid opcode 13
at Receiver.getInfo (/srv/Sharkey/node_modules/.pnpm/ws@8.18.1_bufferutil@4.0.9_utf-8-validate@6.0.5/node_modules/ws/lib/receiver.js:311:26)
at Receiver.startLoop (/srv/Sharkey/node_modules/.pnpm/ws@8.18.1_bufferutil@4.0.9_utf-8-validate@6.0.5/node_modules/ws/lib/receiver.js:155:16)
at Receiver._write (/srv/Sharkey/node_modules/.pnpm/ws@8.18.1_bufferutil@4.0.9_utf-8-validate@6.0.5/node_modules/ws/lib/receiver.js:94:10)
at writeOrBuffer (node:internal/streams/writable:572:12)
at _write (node:internal/streams/writable:501:10)
at Writable.write (node:internal/streams/writable:510:10)
at Socket.socketOnData (/srv/Sharkey/node_modules/.pnpm/ws@8.18.1_bufferutil@4.0.9_utf-8-validate@6.0.5/node_modules/ws/lib/websocket.js:1355:35)
at Socket.emit (node:events:518:28)
at addChunk (node:internal/streams/readable:561:12)
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
at Readable.push (node:internal/streams/readable:392:5)
at TCP.onStreamRead (node:internal/stream_base_commons:189:23) {
code: 'WS_ERR_INVALID_OPCODE',
[Symbol(status-code)]: 1002
}
INFO * [core] The process is going to exit with code 1
node:events:496
throw er; // Unhandled 'error' event
^
RangeError: Invalid WebSocket frame: invalid opcode 13
at Receiver.getInfo (/srv/Sharkey/node_modules/.pnpm/ws@8.18.1_bufferutil@4.0.9_utf-8-validate@6.0.5/node_modules/ws/lib/receiver.js:311:26)
at Receiver.startLoop (/srv/Sharkey/node_modules/.pnpm/ws@8.18.1_bufferutil@4.0.9_utf-8-validate@6.0.5/node_modules/ws/lib/receiver.js:155:16)
at Receiver._write (/srv/Sharkey/node_modules/.pnpm/ws@8.18.1_bufferutil@4.0.9_utf-8-validate@6.0.5/node_modules/ws/lib/receiver.js:94:10)
at writeOrBuffer (node:internal/streams/writable:572:12)
at _write (node:internal/streams/writable:501:10)
at Writable.write (node:internal/streams/writable:510:10)
at Socket.socketOnData (/srv/Sharkey/node_modules/.pnpm/ws@8.18.1_bufferutil@4.0.9_utf-8-validate@6.0.5/node_modules/ws/lib/websocket.js:1355:35)
at Socket.emit (node:events:518:28)
at addChunk (node:internal/streams/readable:561:12)
at readableAddChunkPushByteMode (node:internal/streams/readable:512:3)
at Readable.push (node:internal/streams/readable:392:5)
at TCP.onStreamRead (node:internal/stream_base_commons:189:23)
Emitted 'error' event on WebSocket instance at:
at Receiver.receiverOnError (/srv/Sharkey/node_modules/.pnpm/ws@8.18.1_bufferutil@4.0.9_utf-8-validate@6.0.5/node_modules/ws/lib/websocket.js:1199:15)
at Receiver.emit (node:events:518:28)
at emitErrorNT (node:internal/streams/destroy:170:8)
at emitErrorCloseNT (node:internal/streams/destroy:129:3)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
code: 'WS_ERR_INVALID_OPCODE',
[Symbol(status-code)]: 1002
}
Node.js v22.14.0
ELIFECYCLE Command failed with exit code 1.
This works through some reverse proxies (HAProxy, Caddy),
but not through others (Cloudflare, nginx, Apache(?))
Instead, just hang up if the client violates protocol
Fixes https://101010.pl/@nabijaczleweli/114854334401159070