juegos

Juegos de terminal de Pancho: Catan (TUI, GUI, web, servidor, PicoCalc), ajedrez, calculadora y minijuegos
git clone https://git.lu3dhn.xyz/juegos.git
Log | Files | Refs

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:
Mcatan/linux/online.c | 11++++++++++-
Mcatan/linux/selftest.c | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mcomun/README.md | 4++--
Mcomun/deploy/admin.sh | 6++++--
Mcomun/deploy/install-vps.sh | 6++++--
Mcomun/server/srv.c | 36++++++++++++++++++++++--------------
Mcomun/server/ws.c | 43++++++++++++++++++++++++++++++-------------
Mcomun/server/ws.h | 15++++++++++-----
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. */