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
22 changes: 0 additions & 22 deletions .dockerignore

This file was deleted.

205 changes: 76 additions & 129 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@ on:
branches: [main, master]

jobs:
server:
name: Server Node + smoke test
sito:
name: Sito di presentazione
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: "22"

Expand All @@ -26,187 +24,136 @@ jobs:
node server.js &
echo $! > server.pid
for i in $(seq 1 30); do
if curl -sf -o /dev/null http://localhost:3000/; then
echo "server pronto dopo ${i}s"
exit 0
fi
curl -sf -o /dev/null http://localhost:3000/ && exit 0
sleep 1
done
echo "il server non ha risposto entro 30s"
exit 1
echo "il server non ha risposto entro 30s"; exit 1

- name: La documentazione e' la pagina d'ingresso
- name: La documentazione è servita
run: |
curl -sf http://localhost:3000/ | grep -q 'id="navLinks"'
curl -sf -o /dev/null http://localhost:3000/style.css
curl -sf -o /dev/null http://localhost:3000/script.js
echo "Docs OK"

- name: Il compilatore e' servito su /app
run: |
curl -sf http://localhost:3000/app/ | grep -q 'id="dropzone"'
curl -sf -o /dev/null http://localhost:3000/app/app.js
curl -sf -o /dev/null http://localhost:3000/app/app.css
curl -sf -o /dev/null http://localhost:3000/app/manifest.json
curl -sf -o /dev/null http://localhost:3000/app/sw.js
echo "Frontend OK"

- name: Il vecchio indirizzo /docs reindirizza
- name: Il sito non contiene più il compilatore
run: |
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/docs/)
test "$code" = "301" || { echo "atteso 301, ricevuto $code"; exit 1; }
echo "Redirect OK"
# niente form di upload né endpoint di compilazione
if curl -sf http://localhost:3000/ | grep -q 'id="dropzone"'; then
echo "il sito espone ancora il compilatore"; exit 1
fi
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost:3000/compile)
test "$code" = "404" || { echo "/compile risponde ancora: $code"; exit 1; }
echo "Nessun compilatore sul web OK"

- name: Gli endpoint di download rispondono
- name: Il sito non rimanda a risorse esterne inattese
run: |
curl -sf http://localhost:3000/api/template/quiz.tex | grep -q 'documentclass'
curl -sf -o /dev/null http://localhost:3000/api/download/file/quizstruct.sty
curl -sf -o /dev/null http://localhost:3000/api/download/package.zip
echo "Download OK"

- name: Compilazione di un singolo .tex
run: |
curl -sf -X POST http://localhost:3000/compile \
-F "files=@latex-source/quiz.tex" -o out.json
node -e "
const fs = require('fs');
const d = require('./out.json');
if (!d.success) { console.error(d.log); process.exit(1); }
if (!d.pdf || !d.html) { console.error('output mancanti', d); process.exit(1); }
// gli output arrivano nella risposta, non da un URL sul server
fs.writeFileSync('out.pdf', Buffer.from(d.pdf.base64, 'base64'));
fs.writeFileSync('out.html', Buffer.from(d.html.base64, 'base64'));
console.log('Compile OK');
const html = fs.readFileSync('docs/index.html', 'utf-8');
const host = [...html.matchAll(/https?:\/\/([a-zA-Z0-9.-]+)/g)].map(m => m[1]);
const ammessi = new Set(['github.com', 'www.w3.org']);
const estranei = [...new Set(host)].filter(h => !ammessi.has(h));
if (estranei.length) { console.error('domini inattesi:', estranei); process.exit(1); }
console.log('Nessun dominio esterno inatteso');
"
head -c 4 out.pdf | grep -q '%PDF'
grep -qi '<html' out.html
echo "Output OK"

- name: Gli output non vengono salvati sul server
run: |
# /static non deve più esistere: nessun documento resta raggiungibile
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/static/out.pdf)
test "$code" = "404" || { echo "atteso 404, ricevuto $code"; exit 1; }
node -e "
const d = require('./out.json');
if (d.pdfUrl || d.htmlUrl) { console.error('la risposta espone ancora URL'); process.exit(1); }
console.log('Nessuna persistenza OK');
"
- name: Ferma il server
if: always()
run: kill "$(cat server.pid)" || true

- name: L'HTML generato non esegue script iniettati
app-desktop:
name: App desktop
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"

- name: Install TeX Live
run: |
cat > evil.tex <<'TEX'
\documentclass{article}
\usepackage{enumitem}
\usepackage{quizstruct}
\begin{document}
\begin{quiz}{T<script>window.PWNED=1</script>}
\domanda[1]{D<img src=x onerror="window.PWNED=1">?}
\begin{opzioni}
\rispostacorretta{ok}
\end{opzioni}
\end{quiz}
\end{document}
TEX
curl -sf -X POST http://localhost:3000/compile -F "files=@evil.tex" -o evil.json
node -e "
const d = require('./evil.json');
const html = Buffer.from(d.html.base64, 'base64').toString();
if (/<script>window\.PWNED/.test(html)) { console.error('XSS: script iniettato'); process.exit(1); }
if (/onerror=\"window\.PWNED/.test(html)) { console.error('XSS: handler iniettato'); process.exit(1); }
if (!/&lt;script&gt;/.test(html)) { console.error('atteso payload escapato'); process.exit(1); }
console.log('XSS OK');
"
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
texlive-latex-base texlive-latex-extra \
texlive-fonts-recommended tex4ht

- name: Install dependencies
working-directory: desktop
run: npm install

- name: Raccogli le risorse condivise
working-directory: desktop
run: node prepare.mjs

- name: L'HTML generato non contatta terze parti
- name: I font incorporati sono davvero font
run: |
# I .ttf del repository erano pagine HTML salvate col nome sbagliato:
# embedFont falliva in silenzio e il PDF ripiegava sempre sull'ASCII.
node -e "
const d = require('./out.json');
const html = Buffer.from(d.html.base64, 'base64').toString();
const ext = html.match(/https?:\/\/[a-zA-Z0-9.-]+/g) || [];
if (ext.length) { console.error('richieste esterne:', ext); process.exit(1); }
console.log('Nessuna dipendenza esterna OK');
const fs = require('fs');
for (const f of ['backend/fonts/DejaVuSans.ttf', 'backend/fonts/DejaVuSans-Bold.ttf']) {
const magic = fs.readFileSync(f).subarray(0, 4).toString('hex');
if (!['00010000', '4f54544f', '74727565'].includes(magic)) {
console.error(f, 'non e un font: magic', magic); process.exit(1);
}
console.log(f, 'OK');
}
"

- name: Le intestazioni di sicurezza sono presenti
- name: Prova automatica dell'app
working-directory: desktop
run: |
h=$(curl -sf -o /dev/null -D - http://localhost:3000/)
for header in "content-security-policy" "x-content-type-options" "x-frame-options" "referrer-policy"; do
echo "$h" | grep -qi "^$header:" || { echo "manca $header"; exit 1; }
done
echo "Header OK"
npm install --no-save playwright
xvfb-run -a node smoke-test.mjs

- name: I file non ammessi vengono rifiutati
run: |
echo "x" > payload.exe
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST http://localhost:3000/compile -F "files=@payload.exe")
test "$code" = "415" || { echo "atteso 415, ricevuto $code"; exit 1; }
echo "Validazione upload OK"
- name: L'app si impacchetta
working-directory: desktop
run: npx electron-builder --linux AppImage --publish never

- name: Compilazione multipla produce uno ZIP
- name: L'installer esiste ed è eseguibile
run: |
curl -sf -X POST http://localhost:3000/compile \
-F "files=@latex-source/quiz.tex" \
-F "files=@esempi/matematica.tex" -o multi.json
node -e "
const d = require('./multi.json');
if (!d.success) { console.error(d.log); process.exit(1); }
if (!d.pdf.zip || !d.html.zip) { console.error('atteso uno ZIP', d); process.exit(1); }
console.log('ZIP OK');
"

- name: Ferma il server
if: always()
run: kill "$(cat server.pid)" || true
f=$(ls dist-desktop/*.AppImage)
test -x "$f" || { echo "$f non eseguibile"; exit 1; }
ls -lh "$f"

app:
name: App in file singolo
portatile:
name: Versione portatile in file singolo
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
- uses: actions/setup-node@v4
with:
node-version: "22"

- name: Install dependencies
run: npm install

- name: Costruisci l'app
- name: Costruisci
run: npm run build:app

- name: L'app e' un file solo e non contatta nessuno
- name: È un file solo, senza dipendenze esterne
run: |
test -f dist/texforge.html
node -e "
const fs = require('fs');
const html = fs.readFileSync('dist/texforge.html', 'utf-8');

// niente riferimenti a risorse esterne: deve funzionare offline
const ext = html.match(/(?:src|href)\s*=\s*[\"']https?:\/\/[^\"']+/g) || [];
if (ext.length) { console.error('risorse esterne:', ext); process.exit(1); }

// tutto il necessario deve essere incorporato
for (const atteso of ['PDFLib', 'fontkit', 'generatePdfBytes', 'generateHtmlFromQuiz']) {
if (!html.includes(atteso)) { console.error('manca:', atteso); process.exit(1); }
}

const kb = Math.round(Buffer.byteLength(html) / 1024);
if (kb < 800) { console.error('file sospettosamente piccolo:', kb, 'KB'); process.exit(1); }
console.log('App OK —', kb, 'KB, nessuna dipendenza esterna');
console.log('OK —', Math.round(Buffer.byteLength(html) / 1024), 'KB');
"

- name: L'app compila davvero (browser headless)
- name: Compila davvero, con la rete bloccata
run: |
npm install --no-save playwright
npx playwright install --with-deps chromium
node -e "
const { chromium } = require('playwright');
(async () => {
const b = await chromium.launch();
const ctx = await b.newContext({ acceptDownloads: true });
// blocca la rete: l'app deve cavarsela da sola
const ctx = await b.newContext();
await ctx.route('**', r => r.request().url().startsWith('file://') ? r.continue() : r.abort());
const p = await ctx.newPage();
const errs = [];
Expand Down
Loading
Loading