Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Tests en las tres plataformas donde se publica un binario.
#
# Lo llama tambien deploy.yml antes de tocar produccion: hasta ahora un push a
# main se desplegaba sin ejecutar nada, asi que un commit que rompiese el
# servidor de senializacion llegaba al VPS y solo se notaba al usarlo.
#
# La matriz no es ceremonia: la suite arranca procesos, abre puertos y escribe
# ficheros temporales, y eso es justo lo que se comporta distinto en Windows y en
# macOS. El CLI se publica para las tres.

name: CI

on:
pull_request:
workflow_call:
# En un push a main, deploy.yml ya llama a este workflow antes de desplegar.
# Aqui quedan las rutas que no despliegan nada y que se quedarian sin probar:
# el CLI, la propia suite y los scripts de compilacion y firma.
push:
branches:
- main
paths:
- 'cli/**'
- 'test/**'
- 'scripts/**'
- 'sea-config.json'
- '.github/workflows/ci.yml'

jobs:
test:
name: Tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm

- run: npm ci

# La suite levanta su propio servidor en un puerto efimero y lo apaga al
# acabar, asi que no hay nada que preparar antes.
- run: npm test
24 changes: 23 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,31 @@ on:
- '.github/workflows/deploy.yml'

jobs:
# Los mismos tests que en CI, y el despliegue depende de ellos: el VPS hace
# `git reset --hard origin/main`, asi que sin esta puerta un commit roto llega
# a produccion y solo se descubre usandolo.
tests:
name: Tests
uses: ./.github/workflows/ci.yml

deploy:
name: Deploy to Production
needs: tests
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.2.0
env:
# El dominio publico sale de una variable del repositorio. Escrito en el
# script, cualquiera que bifurque el repo comprobaria la salud del
# servidor de otro. Si no esta puesta, solo se comprueba por dentro del
# contenedor.
HEALTH_URL: ${{ vars.DROP_PUBLIC_URL }}
with:
host: ${{ secrets.DROP_HOST }}
username: ${{ secrets.DROP_USER }}
key: ${{ secrets.DROP_SSH_KEY }}
envs: HEALTH_URL
script: |
set -e
echo "==> Actualizando código en ~/drop..."
Expand All @@ -40,5 +55,12 @@ jobs:

echo "==> Comprobando estado del servicio..."
sleep 3
curl -sf https://drop.oloxx.dev/healthz || docker compose exec -T drop node -e "fetch('http://localhost:3000/healthz').then(r => process.exit(r.ok ? 0 : 1))"
interno() {
docker compose exec -T drop node -e "fetch('http://localhost:3000/healthz').then(r => process.exit(r.ok ? 0 : 1))"
}
if [ -n "$HEALTH_URL" ]; then
curl -sf "$HEALTH_URL/healthz" || interno
else
interno
fi
echo "==> ¡Despliegue completado con éxito!"
29 changes: 9 additions & 20 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,34 +30,23 @@ jobs:
node-version: 22
cache: npm

# La version vive en dos sitios a mano (package.json y la constante
# VERSION de cli/src/cli.js). Si divergen, `drop update` compara contra la
# constante y los clientes no detectan la actualizacion, o la detectan en
# bucle. Mientras eso no se arregle (issue #26), esto lo caza aqui.
- name: Comprobar que el tag, package.json y cli.js coinciden
# La version del CLI sale del package.json (issue #26), asi que aqui solo
# queda contrastar el tag: etiquetar v0.5.3 con el package.json en 0.5.2
# publicaria binarios que se presentan con otra version que la release.
- name: Comprobar que el tag y el package.json coinciden
run: |
TAG="${GITHUB_REF_NAME#v}"
PKG=$(node -p "require('./package.json').version")
CLI=$(node -p "require('fs').readFileSync('cli/src/cli.js','utf8').match(/const VERSION = '([^']+)'/)[1]")
echo "tag=$TAG package.json=$PKG cli.js=$CLI"
if [ "$TAG" != "$PKG" ] || [ "$TAG" != "$CLI" ]; then
echo "::error::La version no coincide en los tres sitios. Corrige y vuelve a etiquetar."
echo "tag=$TAG package.json=$PKG"
if [ "$TAG" != "$PKG" ]; then
echo "::error::El tag $GITHUB_REF_NAME no cuadra con la version $PKG del package.json."
exit 1
fi

- run: npm ci

# `test/signaling.test.mjs` se conecta a un servidor que espera ya
# levantado. Hasta que la suite arranque el suyo (issue #23), se levanta
# aqui y se espera a que /healthz responda.
- name: Levantar el servidor de senializacion
run: |
node server/index.js &
for i in $(seq 1 30); do
curl -sf http://localhost:3000/healthz && break
sleep 1
done

# La suite levanta su propio servidor de senializacion (issue #23): aqui no
# hay que preparar nada.
- run: npm test

# ------------------------------------------------------------------- macOS
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ npm run dev
# Ejecutar el CLI en modo desarrollo
npm run cli -- send mi_archivo.zip

# Ejecutar la suite de tests
# Ejecutar la suite de tests (levanta su propio servidor, no hace falta nada mas)
npm test

# Benchmarks de velocidad
Expand Down
10 changes: 8 additions & 2 deletions cli/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@ import { runSpeedHost, runSpeedGuest } from './speed.js';
import { mapPort } from './upnp.js';
import { newCode, parseCode, randomRoomId, CodeError } from '../../public/shared/codes.js';
import { verifySignature } from './minisign.js';

const VERSION = '0.5.2';
import pkg from '../../package.json' with { type: 'json' };

// La version sale del package.json y de ningun otro sitio. Estuvo escrita a mano
// tambien aqui, y desincronizarlas no es cosmetico: `drop update` compara la
// release de GitHub contra esta constante, asi que una constante vieja deja al
// CLI creyendose desactualizado para siempre, o al reves. Al empaquetar, esbuild
// mete el JSON dentro del bundle, asi que el binario tampoco lee nada en marcha.
const VERSION = pkg.version;

// Clave publica con la que se firma cada release (formato minisign). La privada
// vive como secret del repositorio y solo la toca el workflow de publicacion.
Expand Down
139 changes: 1 addition & 138 deletions public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

import { parseCode, randomSecretWords, formatCode, CodeError } from './shared/codes.js';
import { sasInput, sasWords, formatSas, dtlsFingerprints } from './shared/sas.js';
import { Sha256, sha256Hex } from './shared/sha256.js';

const $ = (sel, root = document) => root.querySelector(sel);

Expand All @@ -61,144 +62,6 @@ const PATH_EVERY = 3000; // cada cuanto refrescamos camino y latencia en la fi

// ---------------------------------------------------------------- utilidades

const SHA256_K = new Uint32Array([
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
]);

class Sha256 {
constructor() {
this.h0 = 0x6a09e667;
this.h1 = 0xbb67ae85;
this.h2 = 0x3c6ef372;
this.h3 = 0xa54ff53a;
this.h4 = 0x510e527f;
this.h5 = 0x9b05688c;
this.h6 = 0x1f83d9ab;
this.h7 = 0x5be0cd19;
this.block = new Uint8Array(64);
this.blockLen = 0;
this.totalLen = 0;
this.w = new Uint32Array(64);
}

_processBlock(b) {
const w = this.w;
for (let i = 0; i < 16; i++) {
const p = i * 4;
w[i] = (b[p] << 24) | (b[p + 1] << 16) | (b[p + 2] << 8) | b[p + 3];
}
for (let i = 16; i < 64; i++) {
const v0 = w[i - 15];
const s0 = ((v0 >>> 7) | (v0 << 25)) ^ ((v0 >>> 18) | (v0 << 14)) ^ (v0 >>> 3);
const v1 = w[i - 2];
const s1 = ((v1 >>> 17) | (v1 << 15)) ^ ((v1 >>> 19) | (v1 << 13)) ^ (v1 >>> 10);
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
}

let a = this.h0, b0 = this.h1, c = this.h2, d = this.h3;
let e = this.h4, f = this.h5, g = this.h6, h = this.h7;

for (let i = 0; i < 64; i++) {
const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
const ch = (e & f) ^ ((~e) & g);
const temp1 = (h + S1 + ch + SHA256_K[i] + w[i]) | 0;
const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
const maj = (a & b0) ^ (a & c) ^ (b0 & c);
const temp2 = (S0 + maj) | 0;

h = g;
g = f;
f = e;
e = (d + temp1) | 0;
d = c;
c = b0;
b0 = a;
a = (temp1 + temp2) | 0;
}

this.h0 = (this.h0 + a) | 0;
this.h1 = (this.h1 + b0) | 0;
this.h2 = (this.h2 + c) | 0;
this.h3 = (this.h3 + d) | 0;
this.h4 = (this.h4 + e) | 0;
this.h5 = (this.h5 + f) | 0;
this.h6 = (this.h6 + g) | 0;
this.h7 = (this.h7 + h) | 0;
}

update(data) {
const bytes = data instanceof Uint8Array
? data
: new Uint8Array(data.buffer || data, data.byteOffset || 0, data.byteLength || data.length);
let offset = 0;
const len = bytes.length;
this.totalLen += len;

if (this.blockLen > 0) {
const needed = 64 - this.blockLen;
if (len >= needed) {
this.block.set(bytes.subarray(0, needed), this.blockLen);
this._processBlock(this.block);
this.blockLen = 0;
offset = needed;
} else {
this.block.set(bytes, this.blockLen);
this.blockLen += len;
return this;
}
}

while (offset + 64 <= len) {
this._processBlock(bytes.subarray(offset, offset + 64));
offset += 64;
}

if (offset < len) {
this.block.set(bytes.subarray(offset), 0);
this.blockLen = len - offset;
}

return this;
}

digest() {
const totalBits = this.totalLen * 8;
this.block[this.blockLen++] = 0x80;
if (this.blockLen > 56) {
this.block.fill(0, this.blockLen);
this._processBlock(this.block);
this.blockLen = 0;
}
this.block.fill(0, this.blockLen, 56);
const hiBits = Math.floor(totalBits / 0x100000000);
const loBits = totalBits >>> 0;
this.block[56] = (hiBits >>> 24) & 0xff;
this.block[57] = (hiBits >>> 16) & 0xff;
this.block[58] = (hiBits >>> 8) & 0xff;
this.block[59] = hiBits & 0xff;
this.block[60] = (loBits >>> 24) & 0xff;
this.block[61] = (loBits >>> 16) & 0xff;
this.block[62] = (loBits >>> 8) & 0xff;
this.block[63] = loBits & 0xff;
this._processBlock(this.block);

const hash = [this.h0, this.h1, this.h2, this.h3, this.h4, this.h5, this.h6, this.h7];
return hash.map((v) => (v >>> 0).toString(16).padStart(8, '0')).join('');
}
}

/** sha256 hexadecimal de una cadena, con el mismo Sha256 que verifica los archivos. */
function sha256Hex(text) {
return new Sha256().update(new TextEncoder().encode(text)).digest();
}

/** Misma formula que `secretProof` de cli/src/crypto.js: los dos extremos deben coincidir. */
function secretProof(nonce, secret) {
return sha256Hex(`drop-proof-v2|${nonce}|${secret}`);
Expand Down
Loading
Loading