commit bf9052015988e5d4bfc0faaeb88f0a90e441ec3b
parent 3c3e071aab529f103116abcc18dcf372bd532faa
Author: Francisco Armando Rosmino <franciscorosmino@rosmino.com.ar>
Date: Sat, 3 Oct 2026 12:17:25 -0300
Servidor: WebSocket mas robusto y ADMIN con nombres con espacio
- ws.c: los frames de control vuelven como mascara de bits (un CLOSE seguido de
PING no se perdia), el PONG devuelve el payload del PING, se exige FIN y <=125
bytes en los de control, se rechaza opcode desconocido, se para en el CLOSE, y
si el payload no entra en out se devuelve lo consumido hasta ahi en vez de
cortar. Subprotocolo con espacios alrededor de las comas.
- srv.c: ADMIN ... RESET <codigo> <clave> <nombre> (el nombre al final, con
espacios), corte explicito de un frame mas grande que el buffer, espera si el
'GET ' llego partido, admin.key nace con 0600.
- admin.sh: codigo en mayusculas, aviso si no puede leer los datos;
install-vps.sh: StateDirectoryMode=0700 y el admin solo para catan.
- online.c: la llave se calcula sobre el nombre como lo guarda el servidor
(sin espacios en las puntas ni '|'), para que coincida con la del admin.
- selftest: SHA-1 en los bordes del relleno, ws_wrap en 125/126/65535/65536,
cabeceras cortadas, ping con payload, CLOSE+PING, out lleno, invalidos.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
8 files changed, 149 insertions(+), 55 deletions(-)
diff --git a/catan/linux/online.c b/catan/linux/online.c
@@ -283,7 +283,16 @@ static void seat_key(const char *name, const char *pass, char out[33])
{
char buf[200], hex[65];
int k = snprintf(buf, sizeof buf, "catan-asiento|");
- for (const char *p = name; *p && k < (int)sizeof buf - 1; p++) buf[k++] = (char)tolower((unsigned char)*p);
+ /* el nombre como lo guarda el servidor (clean_name): sin espacios en las puntas ni
+ * '|' ni controles, asi la llave que calcula el administrador coincide */
+ while (*name == ' ') name++;
+ int k0 = k;
+ for (const char *p = name; *p && k < (int)sizeof buf - 1; p++) {
+ unsigned char ch = (unsigned char)*p;
+ if (ch < 0x20 || ch == '|' || ch == 0x7F) continue;
+ buf[k++] = (char)tolower(ch);
+ }
+ while (k > k0 && buf[k - 1] == ' ') k--;
k += snprintf(buf + k, sizeof buf - (size_t)k, "|%s", pass);
if (k > (int)sizeof buf - 1) k = (int)sizeof buf - 1;
sha256_hex(buf, (size_t)k, hex);
diff --git a/catan/linux/selftest.c b/catan/linux/selftest.c
@@ -1211,6 +1211,13 @@ static int client_frame(int op, const char *data, int n, uint8_t *out, int cap)
return len + 4;
}
+static uint8_t t_ping[WS_CTL_MAX];
+static int t_pinglen;
+static int unwrap(const uint8_t *buf, int n, char *out, int cap, int *got, int *ctl)
+{
+ return ws_unwrap(buf, n, out, cap, got, ctl, t_ping, &t_pinglen);
+}
+
static void t_websocket(void)
{
uint8_t dig[20];
@@ -1222,15 +1229,26 @@ static void t_websocket(void)
static const char *long_msg = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; /* 56 bytes: cae en dos bloques */
sha1(long_msg, strlen(long_msg), dig); hex20(dig, hex);
CHECK(!strcmp(hex, "84983e441c3bd26ebaae4aa1f95129e5e54670f1"));
+ /* los largos alrededor del relleno (55/56 y 63/64 cambian de uno a dos bloques): 'a' x n */
+ static const struct { int n; const char *h; } A[] = {
+ { 55, "c1c8bbdc22796e28c0e15163d20899b65621d65a" }, { 56, "c2db330f6083854c99d4b5bfb6e8f29f201be699" },
+ { 63, "03f09f5b158a7a8cdad920bddc29b81c18a551f5" }, { 64, "0098ba824b5c16427bd7a1122a5a442a25ec644d" },
+ { 119, "ee971065aaa017e0632a8ca6c77bb3bf8b1dfc56" }, { 120, "f34c1488385346a55709ba056ddd08280dd4c6d6" } };
+ for (int i = 0; i < 6; i++) {
+ char a[128];
+ memset(a, 'a', (size_t)A[i].n);
+ sha1(a, (size_t)A[i].n, dig); hex20(dig, hex);
+ CHECK(!strcmp(hex, A[i].h));
+ }
CHECK(base64_encode("", 0, b64, sizeof b64) == 0 && !strcmp(b64, ""));
CHECK(base64_encode("f", 1, b64, sizeof b64) == 4 && !strcmp(b64, "Zg=="));
CHECK(base64_encode("fo", 2, b64, sizeof b64) == 4 && !strcmp(b64, "Zm8="));
CHECK(base64_encode("foobar", 6, b64, sizeof b64) == 8 && !strcmp(b64, "Zm9vYmFy"));
CHECK(base64_encode("foobar", 6, b64, 8) == -1);
- /* handshake del ejemplo de la RFC 6455 */
- const char *req = "GET /ws HTTP/1.1\r\nHost: x\r\nUpgrade: WebSocket\r\nConnection: Upgrade\r\n"
- "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Protocol: binary, base64\r\n"
+ /* handshake del ejemplo de la RFC 6455 (con espacios en la lista de subprotocolos) */
+ const char *req = "GET /ws HTTP/1.1\r\nHost: x\r\nX-Sec-WebSocket-Key: nope\r\nUpgrade: WebSocket\r\nConnection: Upgrade\r\n"
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Protocol: binary , base64\r\n"
"Sec-WebSocket-Version: 13\r\n\r\n";
char resp[512];
int used = 0, n = (int)strlen(req);
@@ -1243,37 +1261,70 @@ static void t_websocket(void)
CHECK(r >= 4 && !strcmp(resp + r - 4, "\r\n\r\n"));
const char *bad = "GET / HTTP/1.1\r\nHost: x\r\n\r\n";
CHECK(ws_handshake(bad, (int)strlen(bad), &used, resp, sizeof resp) == -1 && strstr(resp, "400") != NULL);
+ const char *bad2 = "GET / HTTP/1.1\r\nUpgrade: websocket\r\nX-Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n"; /* el nombre en medio no cuenta */
+ CHECK(ws_handshake(bad2, (int)strlen(bad2), &used, resp, sizeof resp) == -1);
- /* frames: chico, mediano (largo de 16 bits) y grande (64 bits), partidos en dos lecturas */
+ /* ws_wrap: los limites de cada forma del largo */
static char big[70000];
for (int i = 0; i < (int)sizeof big; i++) big[i] = (char)('a' + i % 26);
static uint8_t buf[80000];
static char out[80000];
+ CHECK(ws_wrap(WS_TEXT, big, 125, buf, sizeof buf) == 127 && buf[1] == 125);
+ CHECK(ws_wrap(WS_TEXT, big, 126, buf, sizeof buf) == 130 && buf[1] == 126 && buf[2] == 0 && buf[3] == 126);
+ CHECK(ws_wrap(WS_TEXT, big, 65535, buf, sizeof buf) == 65539 && buf[1] == 126 && buf[2] == 255 && buf[3] == 255);
+ CHECK(ws_wrap(WS_TEXT, big, 65536, buf, sizeof buf) == 65546 && buf[1] == 127 && buf[7] == 1 && buf[8] == 0 && buf[9] == 0);
+ CHECK(ws_wrap(WS_TEXT, big, 300, buf, 303) == -1);
+
+ /* frames: chico, mediano (largo de 16 bits) y grande (64 bits), partidos en dos lecturas */
int got = 0, ctl = 0;
int len = client_frame(WS_TEXT, "HELLO 1 x\n", 10, buf, sizeof buf);
CHECK(len == 16);
- CHECK(ws_unwrap(buf, len, out, sizeof out, &got, &ctl) == len && got == 10 && !memcmp(out, "HELLO 1 x\n", 10) && ctl == 0);
+ CHECK(unwrap(buf, len, out, sizeof out, &got, &ctl) == len && got == 10 && !memcmp(out, "HELLO 1 x\n", 10) && ctl == 0);
+ CHECK(unwrap(buf, 1, out, sizeof out, &got, &ctl) == 0 && got == 0); /* cabecera cortada */
len = client_frame(WS_BINARY, big, 300, buf, sizeof buf);
CHECK(len == 308);
- CHECK(ws_unwrap(buf, len, out, sizeof out, &got, &ctl) == len && got == 300 && !memcmp(out, big, 300));
+ CHECK(unwrap(buf, len, out, sizeof out, &got, &ctl) == len && got == 300 && !memcmp(out, big, 300));
+ CHECK(unwrap(buf, 3, out, sizeof out, &got, &ctl) == 0 && got == 0); /* largo de 16 bits a medias */
len = client_frame(WS_BINARY, big, (int)sizeof big, buf, sizeof buf);
CHECK(len == (int)sizeof big + 14);
+ CHECK(unwrap(buf, 6, out, sizeof out, &got, &ctl) == 0 && got == 0); /* largo de 64 bits a medias */
int half = len / 2;
- CHECK(ws_unwrap(buf, half, out, sizeof out, &got, &ctl) == 0 && got == 0); /* incompleto: no consume */
- CHECK(ws_unwrap(buf, len, out, sizeof out, &got, &ctl) == len && got == (int)sizeof big && !memcmp(out, big, sizeof big));
- /* dos frames seguidos mas un ping, y uno a medias al final */
+ CHECK(unwrap(buf, half, out, sizeof out, &got, &ctl) == 0 && got == 0); /* incompleto: no consume */
+ CHECK(unwrap(buf, len, out, sizeof out, &got, &ctl) == len && got == (int)sizeof big && !memcmp(out, big, sizeof big));
+ /* dos frames seguidos, un ping con payload, y uno a medias al final */
int a = client_frame(WS_TEXT, "AB\n", 3, buf, sizeof buf);
- int b = client_frame(WS_TEXT, "CD\n", 3, buf + a, sizeof buf - (size_t)a);
- int c = client_frame(WS_PING, "", 0, buf + a + b, sizeof buf - (size_t)(a + b));
+ int b = client_frame(WS_CONT, "CD\n", 3, buf + a, sizeof buf - (size_t)a);
+ int c = client_frame(WS_PING, "hola", 4, buf + a + b, sizeof buf - (size_t)(a + b));
int d = client_frame(WS_TEXT, "EF\n", 3, buf + a + b + c, sizeof buf - (size_t)(a + b + c));
- CHECK(ws_unwrap(buf, a + b + c + d - 1, out, sizeof out, &got, &ctl) == a + b + c && got == 6 && !memcmp(out, "AB\nCD\n", 6) && ctl == WS_PING);
- /* sin mascara o con extensiones: invalido; no entra en out: invalido */
+ CHECK(unwrap(buf, a + b + c + d - 1, out, sizeof out, &got, &ctl) == a + b + c && got == 6 && !memcmp(out, "AB\nCD\n", 6));
+ CHECK(ctl == (1 << WS_PING) && t_pinglen == 4 && !memcmp(t_ping, "hola", 4));
+ /* CLOSE seguido de PING: el CLOSE no se pierde y lo de despues no se procesa */
+ a = client_frame(WS_CLOSE, "", 0, buf, sizeof buf);
+ b = client_frame(WS_PING, "", 0, buf + a, sizeof buf - (size_t)a);
+ CHECK(unwrap(buf, a + b, out, sizeof out, &got, &ctl) == a && (ctl & (1 << WS_CLOSE)) && !(ctl & (1 << WS_PING)));
+ /* si out se llena, se devuelve lo consumido hasta ahi y el resto queda para despues */
+ a = client_frame(WS_TEXT, big, 60, buf, sizeof buf);
+ b = client_frame(WS_TEXT, big, 60, buf + a, sizeof buf - (size_t)a);
+ CHECK(unwrap(buf, a + b, out, 100, &got, &ctl) == a && got == 60);
+ CHECK(unwrap(buf + a, b, out, 100, &got, &ctl) == b && got == 60);
+ /* invalidos: sin mascara, extensiones, opcode desconocido, control sin FIN o largo, mas de WS_MAX_FRAME, nunca entra */
CHECK(ws_wrap(WS_TEXT, "AB\n", 3, buf, sizeof buf) == 5);
- CHECK(ws_unwrap(buf, 5, out, sizeof out, &got, &ctl) == -1);
+ CHECK(unwrap(buf, 5, out, sizeof out, &got, &ctl) == -1);
len = client_frame(WS_TEXT, big, 300, buf, sizeof buf);
- CHECK(ws_unwrap(buf, len, out, 100, &got, &ctl) == -1);
+ CHECK(unwrap(buf, len, out, 100, &got, &ctl) == -1);
buf[0] |= 0x40;
- CHECK(ws_unwrap(buf, len, out, sizeof out, &got, &ctl) == -1);
+ CHECK(unwrap(buf, len, out, sizeof out, &got, &ctl) == -1);
+ len = client_frame(3, "x", 1, buf, sizeof buf);
+ CHECK(unwrap(buf, len, out, sizeof out, &got, &ctl) == -1);
+ len = client_frame(WS_PING, "x", 1, buf, sizeof buf);
+ buf[0] &= 0x7F; /* control sin FIN */
+ CHECK(unwrap(buf, len, out, sizeof out, &got, &ctl) == -1);
+ len = client_frame(WS_PING, big, 126, buf, sizeof buf);
+ CHECK(unwrap(buf, len, out, sizeof out, &got, &ctl) == -1);
+ len = client_frame(WS_BINARY, big, 1000, buf, sizeof buf);
+ buf[2] = 0; buf[3] = 0x10; buf[4] = 0; buf[5] = 0; /* largo de 64 bits: 2^36 */
+ buf[1] = 0x80 | 127; memmove(buf + 10, buf + 4, 1004); memset(buf + 2, 0, 8); buf[5] = 0x10;
+ CHECK(unwrap(buf, 1014, out, sizeof out, &got, &ctl) == -1);
}
int app_selftest(void)
diff --git a/comun/README.md b/comun/README.md
@@ -69,8 +69,8 @@ el protocolo de líneas es el mismo. Todo el WebSocket vive en `ws.c` (sin socke
prueba sobre buffers) y `srv.c` lo llama en tres puntos; `-DSRV_NO_WS` lo saca. Como el
navegador exige TLS, delante va Caddy o nginx haciendo `wss://` → `ws://127.0.0.1:puerto`.
-`ADMIN <secreto> RESET <código> <nombre> <llave>` cambia la llave de un asiento (para
-quien se olvidó la contraseña); el secreto está en `<datos>/admin.key` (el servidor lo
+`ADMIN <secreto> RESET <código> <llave> <nombre>` cambia la llave de un asiento (para
+quien se olvidó la contraseña; el nombre va al final porque puede tener espacios); el secreto está en `<datos>/admin.key` (el servidor lo
crea la primera vez). `deploy/admin.sh` (instalado como `<juego>-admin` por
`install-vps.sh`) lo usa y además lista las partidas desde los `meta`.
diff --git a/comun/deploy/admin.sh b/comun/deploy/admin.sh
@@ -24,6 +24,7 @@ seat_key() {
case "$1" in
partidas)
+ [ -r "$DATA" ] || { echo "no puedo leer $DATA (sudo?)" >&2; exit 1; }
for m in "$DATA"/*/meta; do
[ -f "$m" ] || continue
code=$(basename "$(dirname "$m")")
@@ -35,7 +36,7 @@ partidas)
done
;;
reset)
- code=$2; name=$3; pass=$4
+ code=$(printf '%s' "$2" | LC_ALL=C tr 'a-z' 'A-Z'); name=$3; pass=$4
if [ -z "$code" ] || [ -z "$name" ] || [ -z "$pass" ]; then
echo "uso: $0 reset <codigo> <nombre> <contrasena nueva>" >&2
exit 1
@@ -43,7 +44,8 @@ reset)
secret=$(cat "$DATA/admin.key" 2>/dev/null) || { echo "no puedo leer $DATA/admin.key (sudo?)" >&2; exit 1; }
key=$(seat_key "$name" "$pass")
exec 3<>"/dev/tcp/127.0.0.1/$PORT" || exit 1
- printf 'HELLO 1 admin\nADMIN %s RESET %s %s %s\nLEAVE\n' "$secret" "$code" "$name" "$key" >&3
+ # el nombre va al final porque puede tener espacios
+ printf 'HELLO 1 admin\nADMIN %s RESET %s %s %s\nLEAVE\n' "$secret" "$code" "$key" "$name" >&3
# WELCOME, despues OK o ERR
read -r -t 5 line <&3
read -r -t 5 line <&3
diff --git a/comun/deploy/install-vps.sh b/comun/deploy/install-vps.sh
@@ -33,15 +33,17 @@ ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/lib/$NAME
+StateDirectoryMode=0700
[Install]
WantedBy=multi-user.target
UNIT
chmod 644 /etc/systemd/system/$NAME-server.service
# herramienta de administracion (blanquear contrasenas, listar partidas); la llave del
-# asiento usa el prefijo "<juego>-asiento" como el cliente (seat_key en linux/online.c)
+# asiento usa el prefijo "<juego>-asiento" como el cliente (seat_key en linux/online.c).
+# Solo para los juegos que entran con nombre + contrasena (hoy, Catan).
ADMIN=$(dirname "$0")/admin.sh
-if [ -f "$ADMIN" ]; then
+if [ -f "$ADMIN" ] && [ "$NAME" = catan ]; then
sed -e "s/^NAME=.*/NAME=$NAME/" -e "s/^PORT=.*/PORT=$PORT/" -e "s/^KEY_PREFIX=.*/KEY_PREFIX=$NAME-asiento/" \
"$ADMIN" > /usr/local/bin/$NAME-admin
chmod 755 /usr/local/bin/$NAME-admin
diff --git a/comun/server/srv.c b/comun/server/srv.c
@@ -695,16 +695,19 @@ static bool is_key(const char *k)
return true;
}
-/* ADMIN <secreto> RESET <codigo> <nombre> <clave>: cambia la clave del asiento humano
- * con ese nombre (para quien se olvido la contrasena); lo corre el administrador del
- * VPS con <juego>-admin (comun/deploy/admin.sh). El secreto esta en <dir>/admin.key. */
+/* ADMIN <secreto> RESET <codigo> <clave> <nombre>: cambia la clave del asiento humano
+ * con ese nombre (para quien se olvido la contrasena; el nombre va al final porque
+ * puede tener espacios); lo corre el administrador del VPS con <juego>-admin
+ * (comun/deploy/admin.sh). El secreto esta en <dir>/admin.key. */
static void cmd_admin(Server *s, Client *c, const char *args)
{
int ea = s->gm->err_args;
char secret[40], what[16], code[16], name[20], key[40];
- int n = sscanf(args, "%39s %15s %15s %19s %39s", secret, what, code, name, key);
+ int used = 0;
+ int n = sscanf(args, "%39s %15s %15s %39s %n", secret, what, code, key, &used);
if (n < 1 || !s->admin[0] || strcmp(secret, s->admin)) { sendf(s, c, "ERR %d admin", ea); drop(s, c); return; }
- if (n != 5 || strcmp(what, "RESET") || !is_key(key)) { sendf(s, c, "ERR %d uso: ADMIN <secreto> RESET <codigo> <nombre> <clave>", ea); return; }
+ if (n != 4 || strcmp(what, "RESET") || !is_key(key) || !args[used]) { sendf(s, c, "ERR %d uso: ADMIN <secreto> RESET <codigo> <clave> <nombre>", ea); return; }
+ clean_name(args + used, name, sizeof name);
int gi = find_game(s, code);
if (gi < 0) { sendf(s, c, "ERR %d no such game", ea); return; }
SGame *g = s->games[gi];
@@ -777,11 +780,13 @@ Server *srv_open(const GameModule *gm, int port, const char *datadir, bool quiet
if (fscanf(f, "%32s", s->admin) != 1 || !is_key(s->admin)) s->admin[0] = 0;
fclose(f);
}
- if (!s->admin[0] && (f = fopen(path, "w"))) {
- new_token(s->admin);
- fchmod(fileno(f), 0600);
- fprintf(f, "%s\n", s->admin);
- fclose(f);
+ if (!s->admin[0]) {
+ int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0600); /* nace ya con 0600 */
+ if (fd >= 0 && (f = fdopen(fd, "w"))) {
+ new_token(s->admin);
+ fprintf(f, "%s\n", s->admin);
+ fclose(f);
+ } else if (fd >= 0) close(fd);
}
}
s->lfd = (int)socket(AF_INET6, SOCK_STREAM, 0);
@@ -899,15 +904,17 @@ static void ws_input(Server *s, Client *c)
c->wslen -= used;
memmove(c->wsin, c->wsin + used, (size_t)c->wslen);
}
- int got = 0, ctl = 0;
- int used = ws_unwrap(c->wsin, c->wslen, c->in + c->inlen, (int)(sizeof c->in - 1 - (size_t)c->inlen), &got, &ctl);
+ int got = 0, ctl = 0, pinglen = 0;
+ uint8_t ping[WS_CTL_MAX];
+ int used = ws_unwrap(c->wsin, c->wslen, c->in + c->inlen, (int)(sizeof c->in - 1 - (size_t)c->inlen), &got, &ctl, ping, &pinglen);
if (used < 0) { drop(s, c); return; }
+ if (used == 0 && c->wslen >= (int)sizeof c->wsin) { drop(s, c); return; } /* un frame mas grande que el buffer */
c->inlen += got;
c->wslen -= used;
memmove(c->wsin, c->wsin + used, (size_t)c->wslen);
- if (ctl == WS_PING) { uint8_t fr[2]; send_all(s, c, fr, ws_wrap(WS_PONG, 0, 0, fr, sizeof fr)); }
+ if (ctl & (1 << WS_PING)) { uint8_t fr[WS_CTL_MAX + 2]; send_all(s, c, fr, ws_wrap(WS_PONG, ping, pinglen, fr, sizeof fr)); }
if (c->fd >= 0) feed_lines(s, c);
- if (ctl == WS_CLOSE && c->fd >= 0) { uint8_t fr[2]; send_all(s, c, fr, ws_wrap(WS_CLOSE, 0, 0, fr, sizeof fr)); drop(s, c); }
+ if ((ctl & (1 << WS_CLOSE)) && c->fd >= 0) { uint8_t fr[2]; send_all(s, c, fr, ws_wrap(WS_CLOSE, 0, 0, fr, sizeof fr)); drop(s, c); }
}
#endif
@@ -929,6 +936,7 @@ static void read_client(Server *s, Client *c)
c->inlen += (int)r;
#ifndef SRV_NO_WS
/* un navegador: lo primero que manda es un pedido HTTP de upgrade, no HELLO */
+ if (!c->hello && c->inlen < 4 && !memcmp(c->in, "GET ", (size_t)c->inlen)) return; /* puede ser "GE": esperar */
if (!c->hello && c->inlen == (int)r && r >= 4 && !memcmp(c->in, "GET ", 4)) {
memcpy(c->wsin, c->in, (size_t)r);
c->wslen = (int)r;
diff --git a/comun/server/ws.c b/comun/server/ws.c
@@ -8,6 +8,8 @@
#define WS_MAX_FRAME (1 << 20) /* mas que esto es un cliente roto o malicioso */
+static int lc(int ch) { return ch >= 'A' && ch <= 'Z' ? ch + 32 : ch; }
+
/* busca una cabecera (sin distinguir mayusculas) y copia su valor sin espacios */
static int header(const char *req, int n, const char *name, char *out, int cap)
{
@@ -15,7 +17,7 @@ static int header(const char *req, int n, const char *name, char *out, int cap)
for (int i = 0; i + nl + 1 < n; i++) {
if (i > 0 && req[i - 1] != '\n') continue;
int k = 0;
- while (k < nl && (req[i + k] | 32) == (name[k] | 32)) k++;
+ while (k < nl && lc(req[i + k]) == lc(name[k])) k++;
if (k < nl || req[i + nl] != ':') continue;
int p = i + nl + 1;
while (p < n && (req[p] == ' ' || req[p] == '\t')) p++;
@@ -52,12 +54,16 @@ int ws_handshake(const char *req, int n, int *consumed, char *resp, int cap)
sha1(cat, strlen(cat), dig);
char accept[32];
base64_encode(dig, 20, accept, sizeof accept);
- /* el primer subprotocolo ofrecido, si hay (el cliente emscripten manda "binary") */
+ /* el primer subprotocolo ofrecido, si hay (el cliente emscripten manda "binary");
+ * la lista puede traer espacios alrededor de las comas */
char sub[160] = "";
if (header(req, len, "Sec-WebSocket-Protocol", proto, sizeof proto) > 0) {
- char *comma = strchr(proto, ',');
- if (comma) *comma = 0;
- snprintf(sub, sizeof sub, "Sec-WebSocket-Protocol: %s\r\n", proto);
+ char *p = proto;
+ while (*p == ' ' || *p == ',') p++;
+ char *e = p;
+ while (*e && *e != ',' && *e != ' ') e++;
+ *e = 0;
+ if (*p) snprintf(sub, sizeof sub, "Sec-WebSocket-Protocol: %s\r\n", p);
}
int r = snprintf(resp, (size_t)cap,
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
@@ -65,13 +71,14 @@ int ws_handshake(const char *req, int n, int *consumed, char *resp, int cap)
return r >= cap ? -1 : r;
}
-int ws_unwrap(const uint8_t *buf, int n, char *out, int cap, int *outlen, int *ctl)
+int ws_unwrap(const uint8_t *buf, int n, char *out, int cap, int *outlen, int *ctl, uint8_t ping[WS_CTL_MAX], int *pinglen)
{
int pos = 0, o = 0;
*ctl = 0;
+ *pinglen = 0;
while (pos + 2 <= n) {
const uint8_t *f = buf + pos;
- int op = f[0] & 0x0F, masked = f[1] & 0x80, hdr = 2;
+ int op = f[0] & 0x0F, fin = f[0] & 0x80, masked = f[1] & 0x80, hdr = 2;
uint64_t len = f[1] & 0x7F;
if (len == 126) {
if (pos + 4 > n) break;
@@ -84,16 +91,26 @@ int ws_unwrap(const uint8_t *buf, int n, char *out, int cap, int *outlen, int *c
hdr = 10;
}
if (!masked || len > WS_MAX_FRAME || (f[0] & 0x70)) return -1; /* el cliente siempre enmascara; sin extensiones */
- if (masked) hdr += 4;
+ bool data = op == WS_CONT || op == WS_TEXT || op == WS_BINARY;
+ bool control = op == WS_CLOSE || op == WS_PING || op == WS_PONG;
+ if (!data && !control) return -1;
+ if (control && (!fin || len > WS_CTL_MAX)) return -1;
+ if (data && len > (uint64_t)cap) return -1; /* nunca va a entrar */
+ if (data && len > (uint64_t)(cap - o)) break; /* entra, pero en la proxima: primero vaciar out */
+ hdr += 4;
if ((uint64_t)(n - pos) < (uint64_t)hdr + len) break; /* frame incompleto */
const uint8_t *mask = f + hdr - 4, *pay = f + hdr;
- if (op == WS_CONT || op == WS_TEXT || op == WS_BINARY) {
- if ((uint64_t)(cap - o) < len) return -1;
+ if (data) {
for (uint64_t i = 0; i < len; i++) out[o++] = (char)(pay[i] ^ mask[i & 3]);
- } else if (op == WS_CLOSE || op == WS_PING || op == WS_PONG) {
- *ctl = op;
- } else return -1;
+ } else {
+ *ctl |= 1 << op;
+ if (op == WS_PING) {
+ *pinglen = (int)len;
+ for (uint64_t i = 0; i < len; i++) ping[i] = (uint8_t)(pay[i] ^ mask[i & 3]);
+ }
+ }
pos += hdr + (int)len;
+ if (op == WS_CLOSE) break; /* lo que siga ya no importa */
}
*outlen = o;
return pos;
diff --git a/comun/server/ws.h b/comun/server/ws.h
@@ -8,6 +8,7 @@
#include <stdint.h>
enum { WS_CONT = 0, WS_TEXT = 1, WS_BINARY = 2, WS_CLOSE = 8, WS_PING = 9, WS_PONG = 10 };
+#define WS_CTL_MAX 125 /* payload maximo de un frame de control */
/* Pedido HTTP de upgrade (req, n bytes, puede venir incompleto o con frames detras).
* Devuelve el largo de la respuesta "101 Switching Protocols" escrita en resp y deja
@@ -17,11 +18,15 @@ int ws_handshake(const char *req, int n, int *consumed, char *resp, int cap);
/* Desenvuelve los frames completos que haya en buf[0..n) (del cliente: enmascarados).
* El payload de los frames de datos (texto/binario/continuacion) se copia seguido en
- * out (hasta cap bytes; *outlen recibe cuantos). *ctl recibe el opcode del ultimo
- * frame de control visto (WS_CLOSE / WS_PING / WS_PONG) o 0. Devuelve los bytes
- * consumidos de buf (lo que queda es un frame incompleto, para la proxima lectura),
- * o -1 si el flujo es invalido (sin mascara, demasiado grande, no entra en out). */
-int ws_unwrap(const uint8_t *buf, int n, char *out, int cap, int *outlen, int *ctl);
+ * out (hasta cap bytes; *outlen recibe cuantos). *ctl recibe una mascara de bits
+ * (1 << opcode) con los frames de control vistos (WS_CLOSE / WS_PING / WS_PONG); el
+ * payload del ultimo PING queda en ping[] (*pinglen bytes) para devolverlo en el PONG.
+ * Despues de un CLOSE no se procesa nada mas. Devuelve los bytes consumidos de buf: lo
+ * que queda es un frame incompleto (o uno cuyo payload no entra en out todavia), para
+ * la proxima llamada. -1 si el flujo es invalido (sin mascara, extensiones, opcode
+ * desconocido, control sin FIN o de mas de 125 bytes, frame mayor que WS_MAX_FRAME, o
+ * un frame que nunca va a entrar en out). */
+int ws_unwrap(const uint8_t *buf, int n, char *out, int cap, int *outlen, int *ctl, uint8_t ping[WS_CTL_MAX], int *pinglen);
/* Arma un frame del servidor (sin mascara) con ese opcode y payload. Devuelve el largo
* escrito en out o -1 si no entra. */