Compare commits
No commits in common. "42c1a7df8f9b6e9f114904842482fa81f0a66de8" and "14949a377515e625dd9f147ebe6dee78a5df5cab" have entirely different histories.
42c1a7df8f
...
14949a3775
|
|
@ -1,4 +1 @@
|
||||||
data/
|
data
|
||||||
TASKS.org
|
|
||||||
BUGS.org
|
|
||||||
|
|
||||||
|
|
|
||||||
197
CLAUDE.md
197
CLAUDE.md
|
|
@ -1,12 +1,12 @@
|
||||||
# napi — Retroalimentación Personalizada a Estudiantes
|
# napi.qvd.tech — Retroalimentación a Estudiantes
|
||||||
|
|
||||||
## ✅ Estado: PRODUCCIÓN — 2 grupos activos (2026-02-25)
|
## ✅ Estado: MVP FUNCIONAL (2026-02-22)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Contexto
|
## Contexto
|
||||||
|
|
||||||
Proyecto minimalista para proporcionar retroalimentación personalizada a los estudiantes, cerrando el bucle profesor → alumno.
|
Proyecto minimalista para proporcionar retroalimentación personalizada a los estudiantes de DDAW2, cerrando el bucle profesor → alumno.
|
||||||
|
|
||||||
El profesor (Fénix) mantiene ficheros `notas.md` por alumno con:
|
El profesor (Fénix) mantiene ficheros `notas.md` por alumno con:
|
||||||
- Tabla resumen de todas las prácticas (nota + estado)
|
- Tabla resumen de todas las prácticas (nota + estado)
|
||||||
|
|
@ -15,55 +15,121 @@ El profesor (Fénix) mantiene ficheros `notas.md` por alumno con:
|
||||||
|
|
||||||
Los alumnos acceden desde cualquier dispositivo con su navegador, sin instalar nada.
|
Los alumnos acceden desde cualquier dispositivo con su navegador, sin instalar nada.
|
||||||
|
|
||||||
**Sin backend. Sin base de datos. Sin framework. Sin CDN.**
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Grupos Activos
|
## Arquitectura Final
|
||||||
|
|
||||||
| Grupo | URL | Alumnos | Datos local | Datos zzz |
|
|
||||||
|:------|:----|:-------:|:------------|:----------|
|
|
||||||
| **DDAW2** | `https://notas.qu3v3d0.tech` | 19 | `~/napi-data/` | `/var/www/napi/data/` |
|
|
||||||
| **ASIR1** | `https://asir1.qu3v3d0.tech` | 21 | `~/napi-data2/` | `/var/www/napi2/data/` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Arquitectura
|
|
||||||
|
|
||||||
```
|
```
|
||||||
PROFESOR (aldebaran / anka4) SERVIDOR (zzz / qu3v3d0.tech)
|
aldebaran (local) zzz (qu3v3d0.tech)
|
||||||
──────────────────────────── ─────────────────────────────
|
───────────────── ──────────────────
|
||||||
~/napi-data/ ←── sshfs ──→ /var/www/napi/data/ (DDAW2, 19 alumnos)
|
~/napi-data/ sshfs /var/www/napi/data/
|
||||||
~/napi-data2/ ←── sshfs ──→ /var/www/napi2/data/ (ASIR1, 21 alumnos)
|
├── anas/notas.md ←──────────→ ├── anas/notas.md
|
||||||
|
├── pablo/notas.md ├── pablo/notas.md
|
||||||
|
├── miguel/notas.md ├── miguel/notas.md
|
||||||
|
└── .../ (19 alumnos) └── .../ (19 alumnos)
|
||||||
|
|
||||||
/var/www/napi[2]/
|
/var/www/napi/
|
||||||
├── viewer.html
|
├── viewer.html (app completa)
|
||||||
├── marked.min.js
|
└── marked.min.js (renderer local)
|
||||||
└── twemoji.min.js
|
|
||||||
|
|
||||||
Nginx + auth_pam → $remote_user
|
Nginx + libnginx-mod-http-auth-pam
|
||||||
→ data/$remote_user/notas.md
|
→ auth con credenciales SFTP del alumno
|
||||||
→ viewer.html renderiza con marked.js
|
→ $remote_user → sirve /var/www/napi/data/$remote_user/notas.md
|
||||||
|
→ viewer.html renderiza el .md en el navegador (marked.js local)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Stack
|
### Stack
|
||||||
|
|
||||||
| Componente | Tecnología | Dónde |
|
| Componente | Tecnología | Dónde |
|
||||||
|:-----------|:-----------|:------|
|
|:-----------|:-----------|:------|
|
||||||
| **Datos** | Ficheros `notas.md` (Markdown) | zzz |
|
| **Datos** | Ficheros `notas.md` (Markdown) | `zzz:/var/www/napi/data/` |
|
||||||
| **Transporte** | sshfs mounts persistentes (systemd) | aldebaran/anka4 → zzz |
|
| **Transporte** | sshfs mount persistente | aldebaran → zzz |
|
||||||
| **Servidor web** | Nginx (1 server block por grupo) | zzz |
|
| **Servidor web** | Nginx | zzz |
|
||||||
| **Autenticación** | `libnginx-mod-http-auth-pam` | zzz |
|
| **Autenticación** | `libnginx-mod-http-auth-pam` | zzz |
|
||||||
| **Renderer** | `marked.min.js` + `twemoji.min.js` + `viewer.html` | zzz |
|
| **Renderer** | `marked.min.js` (local, sin CDN) + `viewer.html` | zzz |
|
||||||
| **Notificaciones DDAW2** | `nginx-user-config-watcher.sh` (v3) + XMPP | zzz |
|
| **Notificaciones** (watcher nginx) | Python + slixmpp → XMPP | zzz |
|
||||||
| **Notificaciones ASIR1** | `python-upload-watcher.sh` (v7) + XMPP | zzz |
|
|
||||||
| **SSL** | Certificado wildcard `*.qu3v3d0.tech` | zzz |
|
**Sin backend. Sin Python para servir. Sin JSON. Sin Syncthing.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Alumnos
|
## URL de Acceso
|
||||||
|
|
||||||
### DDAW2 (19) — usernames = nombre de pila
|
```
|
||||||
|
https://notas.qu3v3d0.tech
|
||||||
|
```
|
||||||
|
|
||||||
|
Una sola URL para todos los alumnos. Nginx usa `$remote_user` tras auth_pam para servir el `.md` correcto.
|
||||||
|
|
||||||
|
- Alumno ingresa sus credenciales SFTP (mismas que FileZilla)
|
||||||
|
- Ve únicamente sus propias notas
|
||||||
|
- Refresco del navegador = actualización inmediata
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ficheros Clave en zzz
|
||||||
|
|
||||||
|
| Ruta | Descripción |
|
||||||
|
|:-----|:------------|
|
||||||
|
| `/var/www/napi/data/$alumno/notas.md` | Fuente de datos por alumno |
|
||||||
|
| `/var/www/napi/viewer.html` | App completa (fetch + marked.js) |
|
||||||
|
| `/var/www/napi/marked.min.js` | Renderer Markdown local |
|
||||||
|
| `/etc/nginx/sites-enabled/napi` | Config Nginx del servicio |
|
||||||
|
|
||||||
|
`www-data` está en grupo `shadow` (necesario para auth_pam).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ficheros Clave en aldebaran
|
||||||
|
|
||||||
|
| Ruta | Descripción |
|
||||||
|
|:-----|:------------|
|
||||||
|
| `~/napi-data/` | Mount sshfs → `/var/www/napi/data/` en zzz |
|
||||||
|
| `~/.config/systemd/user/home-fenix-napi\x2ddata.mount` | Unit systemd persistente |
|
||||||
|
| `~/napi-data/_plantilla/notas.md` | Plantilla para alumnos nuevos |
|
||||||
|
| `~/napi-data/README.md` | Documentación del proyecto |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Emacs (edición)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. C-x C-f ~/napi-data/pablo/notas.md
|
||||||
|
2. Editar → C-x C-s
|
||||||
|
3. Alumno refresca navegador → cambios visibles al instante
|
||||||
|
```
|
||||||
|
|
||||||
|
El sshfs hace que guardar en aldebaran sea equivalente a escribir directamente en zzz.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Formato notas.md
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Notas — NombreAlumno
|
||||||
|
|
||||||
|
> 🏫 Módulo: DDAW2
|
||||||
|
> 📅 Última actualización: FECHA
|
||||||
|
|
||||||
|
## 📊 Resumen
|
||||||
|
| Práctica | Título | Nota | Estado |
|
||||||
|
| P2.3 | Nginx via SFTP | 7/10 | ✅ |
|
||||||
|
...
|
||||||
|
|
||||||
|
## P2.7 — Multi-sitio Web con Nginx
|
||||||
|
**Nota: 9/10**
|
||||||
|
[feedback personalizado]
|
||||||
|
|
||||||
|
### Criterios
|
||||||
|
| Criterio | Puntos | Estado |
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Ver `_plantilla/notas.md` y cualquier alumno como referencia.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alumnos Activos (19) — DDAW2
|
||||||
|
|
||||||
```
|
```
|
||||||
anas, carlos, carlosv, daniel, danieln, erick, evelin, gianfranco,
|
anas, carlos, carlosv, daniel, danieln, erick, evelin, gianfranco,
|
||||||
|
|
@ -71,53 +137,48 @@ giorgio, joel, jorge, josue, juanan, juanjesus, kasandra, marius,
|
||||||
miguel, pablo, patrick
|
miguel, pablo, patrick
|
||||||
```
|
```
|
||||||
|
|
||||||
SFTP chroot: `/home/USER/html/`
|
Credenciales SFTP en `~/EducaMadrid/DDAW2/PRACTICA2.4-despliegue-web-HTTP-y-HTTPS/README.md`.
|
||||||
|
|
||||||
### ASIR1 (21) — usernames = apellido en minúsculas
|
|
||||||
|
|
||||||
```
|
|
||||||
barja, barrios, cayo, contrera, duque, florea, gomes, izquierdo,
|
|
||||||
jara, lillo, linares, macedo, martinez, munoz, olcina, ponce,
|
|
||||||
posada, quiroz, reynoso, sierra, torrero
|
|
||||||
```
|
|
||||||
|
|
||||||
SFTP chroot: `/home/USER/python/` · Contraseñas: leet-speak (`a→4, e→3, i→1, o→0`)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## SSH / Conexión a zzz
|
## SSH / Conexión a zzz
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh fenix@qu3v3d0.tech # clave sin -i, fenix tiene sudo
|
ssh fenix@qu3v3d0.tech # clave sin -i
|
||||||
|
```
|
||||||
|
|
||||||
|
`fenix` tiene sudo en zzz. Alumnos: grupo `sftpusers`, chroot `/home/$user`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Añadir Alumno Nuevo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. En zzz: crear usuario SFTP (ver README de P2.4 para el script completo)
|
||||||
|
sudo useradd -m -d /home/$USER -s /usr/sbin/nologin -G sftpusers,www-data $USER
|
||||||
|
|
||||||
|
# 2. En aldebaran: crear carpeta de notas
|
||||||
|
mkdir ~/napi-data/$USER
|
||||||
|
cp ~/napi-data/_plantilla/notas.md ~/napi-data/$USER/notas.md
|
||||||
|
# Editar con Emacs y personalizar
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Ficheros Clave
|
## Prácticas Documentadas
|
||||||
|
|
||||||
### En zzz
|
| Práctica | Directorio local | Descripción |
|
||||||
|
|:---------|:-----------------|:------------|
|
||||||
| Ruta | Descripción |
|
| P2.3 | `~/EducaMadrid/DDAW2/PRACTICA2.3-despliegue-nginx-via-sftp/` | Nginx config via SFTP + watcher inotify + XMPP |
|
||||||
|:-----|:------------|
|
| P2.4 | `~/EducaMadrid/DDAW2/PRACTICA2.4-despliegue-web-HTTP-y-HTTPS/` | HTTP + HTTPS con cert wildcard autofirmado |
|
||||||
| `/var/www/napi/` | App DDAW2 (viewer + marked + twemoji + data/) |
|
| P2.5 | `~/EducaMadrid/DDAW2/PRACTICA2.5-despliegue-web-bloquear-CDN-JS+servir-MD/` | CSP + bloqueo CDN + marked.js local |
|
||||||
| `/var/www/napi2/` | App ASIR1 (idem) |
|
| P2.6 | `~/EducaMadrid/DDAW2/PRACTICA2.6-Hextris-compresion-cache-y-mejora-de-rendimiento/` | Hextris + gzip + caché |
|
||||||
| `/etc/nginx/sites-enabled/napi` | Server block DDAW2 |
|
| P2.7 | `~/EducaMadrid/DDAW2/PRACTICA2.7-MULTI-SITIO-WEB-NGINX@zzz/` | Multi-sitio: Hextris + App propia + CSP + cabeceras |
|
||||||
| `/etc/nginx/sites-enabled/napi2` | Server block ASIR1 |
|
|
||||||
| `/usr/local/bin/python-upload-watcher.sh` | Watcher ASIR1 (v7) |
|
|
||||||
| `/usr/local/bin/nginx-user-config-watcher.sh` | Watcher DDAW2 (v3) |
|
|
||||||
| `/usr/local/bin/xmpp-notify.py` | Bot XMPP one-shot |
|
|
||||||
|
|
||||||
### En aldebaran/anka4
|
|
||||||
|
|
||||||
| Ruta | Descripción |
|
|
||||||
|:-----|:------------|
|
|
||||||
| `~/napi-data/` | sshfs → DDAW2 data |
|
|
||||||
| `~/napi-data2/` | sshfs → ASIR1 data |
|
|
||||||
| `~/.config/systemd/user/home-fenix-napi\x2ddata.mount` | Mount DDAW2 |
|
|
||||||
| `~/.config/systemd/user/home-fenix-napi\x2ddata2.mount` | Mount ASIR1 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Pendiente
|
## Pendiente
|
||||||
|
|
||||||
|
- [ ] Rellenar notas de pablo (P2.3 → P2.6 pendientes de detallar)
|
||||||
|
- [ ] Considerar CSS más elaborado para viewer.html (opcional)
|
||||||
- [ ] Renovar certificado SSL wildcard cuando expire (generado 2026-02-04, válido 365 días)
|
- [ ] Renovar certificado SSL wildcard cuando expire (generado 2026-02-04, válido 365 días)
|
||||||
|
|
|
||||||
567
README.md
567
README.md
|
|
@ -1,14 +1,17 @@
|
||||||
# napi — Retroalimentación Personalizada a Estudiantes
|
# napi-data — Notas Personalizadas para Estudiantes
|
||||||
|
|
||||||
> Sistema minimalista para servir feedback personalizado a cada alumno,
|
> Sistema minimalista para servir retroalimentación personalizada a cada alumno,
|
||||||
> directamente desde ficheros Markdown editados con Emacs.
|
> directamente desde ficheros Markdown editados con Emacs.
|
||||||
|
|
||||||
**Estado:** ✅ Producción — 2 grupos activos (2026-02-25)
|
**Estado:** ✅ Producción — `https://notas.qu3v3d0.tech` (2026-02-22)
|
||||||
|
|
||||||
| Grupo | URL | Alumnos |
|
---
|
||||||
|:------|:----|:-------:|
|
|
||||||
| **DDAW2** — Despliegue Aplicaciones Web | `https://notas.qu3v3d0.tech` | 19 |
|
## ¿Qué hace esto?
|
||||||
| **ASIR1** — Programación | `https://asir1.qu3v3d0.tech` | 21 |
|
|
||||||
|
Cada alumno entra en `https://notas.TU_DOMINIO` con sus credenciales SFTP y ve
|
||||||
|
**únicamente sus propias notas** en formato HTML renderizado. El profesor edita
|
||||||
|
un fichero `.md` desde Emacs y el alumno lo ve al refrescar el navegador.
|
||||||
|
|
||||||
**Sin backend. Sin base de datos. Sin framework. Sin CDN.**
|
**Sin backend. Sin base de datos. Sin framework. Sin CDN.**
|
||||||
|
|
||||||
|
|
@ -17,334 +20,400 @@
|
||||||
## Arquitectura
|
## Arquitectura
|
||||||
|
|
||||||
```
|
```
|
||||||
PROFESOR (aldebaran / anka4)
|
PROFESOR (aldebaran / máquina local)
|
||||||
|
────────────────────────────────────
|
||||||
|
~/napi-data/ ← sshfs mount
|
||||||
|
├── _plantilla/notas.md
|
||||||
|
├── alumno01/notas.md ──┐
|
||||||
|
├── alumno02/notas.md ──┤ escribe con Emacs
|
||||||
|
└── alumnoN/notas.md ──┘ C-x C-s → visible al instante
|
||||||
|
|
||||||
|
│ sshfs (SSH port 22)
|
||||||
|
▼
|
||||||
|
|
||||||
|
SERVIDOR (zzz / Debian VPS)
|
||||||
────────────────────────────
|
────────────────────────────
|
||||||
~/napi-data/ ← sshfs mount (DDAW2)
|
/var/www/api/
|
||||||
├── _plantilla/notas.md
|
├── viewer.html ← app completa (~40 líneas)
|
||||||
├── anas/notas.md
|
├── marked.min.js ← renderer Markdown local (sin CDN)
|
||||||
└── ... (19 alumnos)
|
└── data/
|
||||||
|
├── alumno01/notas.md
|
||||||
~/napi-data2/ ← sshfs mount (ASIR1)
|
├── alumno02/notas.md
|
||||||
├── _plantilla/notas.md
|
└── alumnoN/notas.md
|
||||||
├── barja/notas.md
|
|
||||||
└── ... (21 alumnos)
|
|
||||||
|
|
||||||
│ sshfs (SSH port 22)
|
|
||||||
▼
|
|
||||||
|
|
||||||
SERVIDOR (zzz / qu3v3d0.tech)
|
|
||||||
─────────────────────────────
|
|
||||||
/var/www/napi/ ← DDAW2
|
|
||||||
├── viewer.html
|
|
||||||
├── marked.min.js
|
|
||||||
├── twemoji.min.js
|
|
||||||
└── data/$alumno/notas.md
|
|
||||||
|
|
||||||
/var/www/napi2/ ← ASIR1
|
|
||||||
├── viewer.html
|
|
||||||
├── marked.min.js
|
|
||||||
├── twemoji.min.js
|
|
||||||
└── data/$alumno/notas.md
|
|
||||||
|
|
||||||
Nginx + libnginx-mod-http-auth-pam
|
Nginx + libnginx-mod-http-auth-pam
|
||||||
→ auth con credenciales SFTP del alumno
|
↓ auth con credenciales SFTP del alumno
|
||||||
→ $remote_user → sirve data/$remote_user/notas.md
|
↓ $remote_user = "alumno01"
|
||||||
→ viewer.html renderiza el .md con marked.js + twemoji
|
↓ sirve data/alumno01/notas.md
|
||||||
|
↓ viewer.html lo renderiza en el navegador
|
||||||
|
|
||||||
ALUMNO (cualquier dispositivo)
|
ALUMNO (cualquier dispositivo)
|
||||||
──────────────────────────────
|
───────────────────────────────
|
||||||
https://notas.qu3v3d0.tech → DDAW2
|
https://notas.TU_DOMINIO
|
||||||
https://asir1.qu3v3d0.tech → ASIR1
|
→ login con sus credenciales SFTP
|
||||||
→ login con credenciales SFTP
|
|
||||||
→ ve sus notas en HTML
|
→ ve sus notas en HTML
|
||||||
→ refresca → cambios inmediatos
|
→ refresca → cambios inmediatos
|
||||||
```
|
```
|
||||||
|
|
||||||
### Stack
|
|
||||||
|
|
||||||
| Componente | Tecnología | Dónde |
|
|
||||||
|:-----------|:-----------|:------|
|
|
||||||
| **Datos** | Ficheros `notas.md` (Markdown) | zzz |
|
|
||||||
| **Transporte** | sshfs mounts persistentes (systemd) | aldebaran/anka4 → zzz |
|
|
||||||
| **Servidor web** | Nginx (1 server block por grupo) | zzz |
|
|
||||||
| **Autenticación** | `libnginx-mod-http-auth-pam` | zzz |
|
|
||||||
| **Renderer** | `marked.min.js` + `twemoji.min.js` + `viewer.html` | zzz |
|
|
||||||
| **Notificaciones DDAW2** | `nginx-user-config-watcher.sh` + XMPP | zzz |
|
|
||||||
| **Notificaciones ASIR1** | `python-upload-watcher.sh` (v7) + XMPP | zzz |
|
|
||||||
| **SSL** | Certificado wildcard `*.qu3v3d0.tech` | zzz |
|
|
||||||
| **DNS** | Wildcard `*.qu3v3d0.tech` → `161.22.44.104` | DNS |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Grupos y Alumnos
|
## Gestión de registros DNS
|
||||||
|
|
||||||
### DDAW2 — Despliegue de Aplicaciones Web (19 alumnos)
|
- Se requiere un https://TU_DOMINIO ad-hoc
|
||||||
|
|
||||||
- **URL:** `https://notas.qu3v3d0.tech`
|
- Por simplicidad, necesitas apuntar TODOS los subodminios a la dirección IP del servidor (usa 'wildcard' - '*')
|
||||||
- **Datos:** `~/napi-data/` → `zzz:/var/www/napi/data/`
|
|
||||||
- **Usernames:** nombre de pila en minúsculas
|
|
||||||
- **SFTP chroot:** `/home/USER/html/`
|
|
||||||
|
|
||||||
```
|
## Requisitos del servidor (Debian)
|
||||||
anas, carlos, carlosv, daniel, danieln, erick, evelin, gianfranco,
|
|
||||||
giorgio, joel, jorge, josue, juanan, juanjesus, kasandra, marius,
|
|
||||||
miguel, pablo, patrick
|
|
||||||
```
|
|
||||||
|
|
||||||
### ASIR1 — Programación (21 alumnos)
|
|
||||||
|
|
||||||
- **URL:** `https://asir1.qu3v3d0.tech`
|
|
||||||
- **Datos:** `~/napi-data2/` → `zzz:/var/www/napi2/data/`
|
|
||||||
- **Usernames:** apellido en minúsculas (sin tildes)
|
|
||||||
- **Contraseñas:** leet-speak del apellido (`a→4, e→3, i→1, o→0`)
|
|
||||||
- **SFTP chroot:** `/home/USER/python/`
|
|
||||||
|
|
||||||
| Username | Alumno | Username | Alumno |
|
|
||||||
|:---------|:-------|:---------|:-------|
|
|
||||||
| **barja** | Alex Barja | **martinez** | Daniel Martínez |
|
|
||||||
| **barrios** | Andrés Barrios | **munoz** | Jordy Muñoz |
|
|
||||||
| **cayo** | Jared Cayo | **olcina** | Jorge Olcina |
|
|
||||||
| **contrera** | Luciano Contrera | **ponce** | Francisco Ponce |
|
|
||||||
| **duque** | Jorge Duque | **posada** | Santiago Posada |
|
|
||||||
| **florea** | Alejandro Florea | **quiroz** | Alexander Quiroz |
|
|
||||||
| **gomes** | Gabriel Gomes | **reynoso** | Jorge Reynoso |
|
|
||||||
| **izquierdo** | Daniel Izquierdo | **sierra** | Leonel Sierra |
|
|
||||||
| **jara** | María Jara | **torrero** | Mario Torrero |
|
|
||||||
| **lillo** | Fernando Lillo | | |
|
|
||||||
| **linares** | Christopher Linares | | |
|
|
||||||
| **macedo** | Eduardo Macedo | | |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Notificaciones XMPP
|
|
||||||
|
|
||||||
Ambos grupos tienen watchers en zzz que notifican al profesor via XMPP (`jla@librebits.info`) cuando un alumno sube ficheros por SFTP.
|
|
||||||
|
|
||||||
### DDAW2: `nginx-user-config-watcher.sh` (v3)
|
|
||||||
|
|
||||||
Monitoriza `/home/USER/html/*.conf` — valida, despliega y analiza CSP de configuraciones Nginx.
|
|
||||||
|
|
||||||
### ASIR1: `python-upload-watcher.sh` (v7)
|
|
||||||
|
|
||||||
Monitoriza `/home/USER/python/` — detecta entregas de prácticas de Programación.
|
|
||||||
|
|
||||||
**Características:**
|
|
||||||
|
|
||||||
- **Batching por carpeta** — Si un alumno sube `PRACTICA3.1/` con N ficheros, envía **un solo mensaje** con el listado completo (espera 10s de silencio)
|
|
||||||
- **Re-entregas** — Detecta delete+recreate de una carpeta como "re-entrega"
|
|
||||||
- **Nombre completo** — Notifica `[María Jara]` en vez de `[jara]`
|
|
||||||
- **Windows-safe** — Ignora `Thumbs.db`, `desktop.ini`, `.DS_Store`, `*.tmp`
|
|
||||||
- **Python-safe** — Ignora `__pycache__/`, `*.pyc`, `*.pyo`
|
|
||||||
- **Dev-safe** — Ignora `.git/`, `.venv/`, `node_modules/`, `.idea/`, `.vscode/`
|
|
||||||
|
|
||||||
**Ejemplo de notificación:**
|
|
||||||
|
|
||||||
```
|
|
||||||
📁 [Andrés Barrios] Entrega ASIR1: PRACTICA3.1/ (4 ficheros)
|
|
||||||
🐍 main.py (2048 bytes)
|
|
||||||
🐍 utils.py (1024 bytes)
|
|
||||||
📝 README.md (512 bytes)
|
|
||||||
📄 requisitos.txt (128 bytes)
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
📁 [Andrés Barrios] ♻️ Re-entrega ASIR1: PRACTICA3.1/ (3 ficheros)
|
|
||||||
🐍 main.py (2100 bytes)
|
|
||||||
📝 README.md (600 bytes)
|
|
||||||
📄 requisitos.txt (128 bytes)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Workflow del Profesor
|
|
||||||
|
|
||||||
```
|
|
||||||
1. C-x C-f ~/napi-data2/barrios/notas.md ← abrir en Emacs
|
|
||||||
2. Editar feedback, notas, próximos pasos
|
|
||||||
3. C-x C-s ← guardar
|
|
||||||
4. Alumno refresca el navegador ← cambios visibles
|
|
||||||
```
|
|
||||||
|
|
||||||
**No hay paso 5.** El sshfs hace que guardar localmente sea equivalente a escribir en zzz.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Requisitos del Servidor (Debian)
|
|
||||||
|
|
||||||
- Debian 11/12/...
|
- Debian 11/12/...
|
||||||
- Nginx + `libnginx-mod-http-auth-pam`
|
- Nginx
|
||||||
- Certificado SSL (wildcard recomendado para múltiples subdominios)
|
- `libnginx-mod-http-auth-pam`
|
||||||
- Usuarios SFTP con chroot (`grupo sftpusers`)
|
- `libpam-runtime` (incluido por defecto)
|
||||||
- `www-data` en grupo `shadow` (necesario para auth_pam)
|
- Certificado SSL (Let's Encrypt o autofirmado)
|
||||||
- Python 3 + `slixmpp` (para notificaciones XMPP)
|
- Usuarios SFTP ya configurados en el sistema (grupo `sftpusers`) usando 'chroot' , partiendo de que cada estudiante tiene su 'usuario' del sistema.
|
||||||
- `inotify-tools` (para los watchers)
|
|
||||||
- SSH accesible desde la máquina del profesor (para sshfs)
|
- SSH accesible desde la máquina del profesor (para sshfs)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Despliegue Rápido de un Nuevo Grupo
|
## Despliegue desde cero en un Debian nuevo
|
||||||
|
|
||||||
### 1. En zzz: crear la app web
|
### 1. Instalar dependencias
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo mkdir -p /var/www/napiN/data
|
sudo apt update
|
||||||
sudo cp /var/www/napi/viewer.html /var/www/napiN/
|
sudo apt install -y nginx libnginx-mod-http-auth-pam
|
||||||
sudo cp /var/www/napi/marked.min.js /var/www/napiN/
|
|
||||||
sudo cp /var/www/napi/twemoji.min.js /var/www/napiN/
|
|
||||||
sudo chown fenix:www-data /var/www/napiN/data
|
|
||||||
sudo chmod 775 /var/www/napiN/data
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. En zzz: crear server block Nginx
|
### 2. Crear estructura de directorios en el servidor
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /var/www/api/data
|
||||||
|
sudo chown -R www-data:www-data /var/www/api
|
||||||
|
sudo chmod 755 /var/www/api
|
||||||
|
sudo chmod 755 /var/www/api/data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Desplegar viewer.html y marked.min.js
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copiar viewer.html al servidor (ver sección "Ficheros de la app" más abajo)
|
||||||
|
sudo cp viewer.html /var/www/api/viewer.html
|
||||||
|
sudo chown www-data:www-data /var/www/api/viewer.html
|
||||||
|
|
||||||
|
# Descargar marked.min.js (o copiar desde backup)
|
||||||
|
# https://cdn.jsdelivr.net/npm/marked/marked.min.js
|
||||||
|
sudo wget -O /var/www/api/marked.min.js \
|
||||||
|
https://cdn.jsdelivr.net/npm/marked/marked.min.js
|
||||||
|
sudo chown www-data:www-data /var/www/api/marked.min.js
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Una vez descargado `marked.min.js`, la app funciona **sin conexión a CDNs**
|
||||||
|
> — es el punto del diseño. No actualices el fichero sin probarlo primero.
|
||||||
|
|
||||||
|
### 4. Añadir www-data al grupo shadow (para auth_pam)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG shadow www-data
|
||||||
|
# Reiniciar nginx para que tome el cambio de grupo
|
||||||
|
sudo systemctl restart nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Configurar Nginx
|
||||||
|
|
||||||
|
Crear `/etc/nginx/sites-available/napi`:
|
||||||
|
|
||||||
```nginx
|
```nginx
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name SUBDOMINIO.qu3v3d0.tech;
|
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name SUBDOMINIO.qu3v3d0.tech;
|
server_name notas.TU_DOMINIO;
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/certs/qu3v3d0.tech.crt;
|
# --- SSL ---
|
||||||
ssl_certificate_key /etc/ssl/private/qu3v3d0.tech.key;
|
ssl_certificate /etc/ssl/certs/TU_DOMINIO.crt;
|
||||||
|
ssl_certificate_key /etc/ssl/private/TU_DOMINIO.key;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
|
||||||
root /var/www/napiN;
|
root /var/www/api;
|
||||||
|
index viewer.html;
|
||||||
|
|
||||||
auth_pam "Notas GRUPO";
|
# --- Autenticación PAM ---
|
||||||
|
auth_pam "Notas DDAW2";
|
||||||
auth_pam_service_name "common-auth";
|
auth_pam_service_name "common-auth";
|
||||||
|
|
||||||
location = / { try_files /viewer.html =404; }
|
# --- Servir notas del alumno autenticado ---
|
||||||
|
|
||||||
location = /notas.md {
|
location = /notas.md {
|
||||||
alias /var/www/napiN/data/$remote_user/notas.md;
|
alias /var/www/api/data/$remote_user/notas.md;
|
||||||
default_type text/plain;
|
default_type text/plain;
|
||||||
charset utf-8;
|
charset utf-8;
|
||||||
add_header Cache-Control "no-cache";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(js|css)$ { expires 7d; }
|
# --- Assets estáticos (viewer + marked) ---
|
||||||
location / { return 404; }
|
location ~* \.(html|js)$ {
|
||||||
|
auth_pam off; # viewer.html y marked.min.js son públicos
|
||||||
|
expires 1h;
|
||||||
|
}
|
||||||
|
|
||||||
|
access_log /var/log/nginx/napi-access.log;
|
||||||
|
error_log /var/log/nginx/napi-error.log;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirección HTTP → HTTPS
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name notas.TU_DOMINIO;
|
||||||
|
return 301 https://$server_name$request_uri;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ln -s /etc/nginx/sites-available/napiN /etc/nginx/sites-enabled/
|
sudo ln -s /etc/nginx/sites-available/napi /etc/nginx/sites-enabled/
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
sudo nginx -t && sudo systemctl reload nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. En zzz: crear usuarios SFTP
|
### 6. Crear carpeta de datos para cada alumno
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
for user in alumno1 alumno2 alumnoN; do
|
# Para un alumno:
|
||||||
sudo useradd -m -d /home/$user -s /usr/sbin/nologin -G sftpusers,www-data $user
|
ALUMNO="alumno01"
|
||||||
echo "$user:CONTRASEÑA" | sudo chpasswd
|
sudo mkdir -p /var/www/api/data/$ALUMNO
|
||||||
sudo mkdir -p /home/$user/CARPETA # html/ o python/ según grupo
|
sudo cp /var/www/api/data/_plantilla/notas.md /var/www/api/data/$ALUMNO/notas.md
|
||||||
sudo chown root:root /home/$user
|
sudo chown -R www-data:www-data /var/www/api/data/$ALUMNO
|
||||||
sudo chmod 755 /home/$user
|
|
||||||
sudo chown $user:www-data /home/$user/CARPETA
|
# Para todos los alumnos de una vez (si ya existen como usuarios del sistema):
|
||||||
sudo chmod 775 /home/$user/CARPETA
|
for user in $(getent group sftpusers | cut -d: -f4 | tr ',' ' '); do
|
||||||
|
sudo mkdir -p /var/www/api/data/$user
|
||||||
|
sudo cp /var/www/api/data/_plantilla/notas.md /var/www/api/data/$user/notas.md 2>/dev/null || true
|
||||||
|
sudo chown -R www-data:www-data /var/www/api/data/$user
|
||||||
|
echo "✅ $user"
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Copiar datos de alumnos y montar sshfs
|
### 7. Verificar que funciona
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Copiar carpetas con notas.md al servidor
|
# Probar autenticación y respuesta
|
||||||
scp -r ~/napi-dataN/* fenix@qu3v3d0.tech:/var/www/napiN/data/
|
curl -u alumno01:SU_CONTRASEÑA -sk https://notas.TU_DOMINIO/notas.md | head -5
|
||||||
|
|
||||||
# Crear unit systemd sshfs (~/.config/systemd/user/)
|
# Debe devolver las primeras líneas del notas.md del alumno
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configurar sshfs en la máquina del profesor
|
||||||
|
|
||||||
|
### Instalar sshfs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian/Ubuntu
|
||||||
|
sudo apt install sshfs
|
||||||
|
|
||||||
|
# Fedora/RHEL
|
||||||
|
sudo dnf install fuse-sshfs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Crear punto de montaje
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/napi-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Montar manualmente (prueba)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sshfs USUARIO@TU_SERVIDOR:/var/www/api/data ~/napi-data \
|
||||||
|
-o reconnect,ServerAliveInterval=15,ServerAliveCountMax=3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Montaje persistente con systemd (recomendado)
|
||||||
|
|
||||||
|
Crear `~/.config/systemd/user/home-TUUSUARIO-napi\x2ddata.mount`
|
||||||
|
(sustituye `TUUSUARIO` por tu usuario real, ej: `fenix`):
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=napi-data → servidor:/var/www/api/data (sshfs)
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Mount]
|
||||||
|
What=USUARIO@TU_SERVIDOR:/var/www/api/data
|
||||||
|
Where=/home/TUUSUARIO/napi-data
|
||||||
|
Type=fuse.sshfs
|
||||||
|
Options=reconnect,ServerAliveInterval=15,ServerAliveCountMax=3
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
systemctl --user daemon-reload
|
systemctl --user daemon-reload
|
||||||
systemctl --user enable --now 'home-fenix-napi\x2ddataN.mount'
|
systemctl --user enable --now 'home-TUUSUARIO-napi\x2ddata.mount'
|
||||||
|
|
||||||
|
# Verificar
|
||||||
|
systemctl --user status 'home-TUUSUARIO-napi\x2ddata.mount'
|
||||||
|
```
|
||||||
|
|
||||||
|
> 💡 El nombre de la unit debe coincidir exactamente con la ruta del `Where`
|
||||||
|
> (reemplazando `/` por `-` y los guiones por `\x2d`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow del profesor
|
||||||
|
|
||||||
|
```
|
||||||
|
1. C-x C-f ~/napi-data/pablo/notas.md ← abrir en Emacs
|
||||||
|
2. Editar feedback, notas, próximos pasos
|
||||||
|
3. C-x C-s ← guardar
|
||||||
|
4. Alumno refresca el navegador ← cambios visibles
|
||||||
|
```
|
||||||
|
|
||||||
|
**No hay paso 5.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Añadir un alumno nuevo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# En el servidor:
|
||||||
|
ALUMNO="nuevo_alumno"
|
||||||
|
sudo mkdir -p /var/www/api/data/$ALUMNO
|
||||||
|
sudo cp /var/www/api/data/_plantilla/notas.md /var/www/api/data/$ALUMNO/notas.md
|
||||||
|
sudo chown -R www-data:www-data /var/www/api/data/$ALUMNO
|
||||||
|
|
||||||
|
# En la máquina del profesor (aparece automáticamente via sshfs):
|
||||||
|
ls ~/napi-data/$ALUMNO/ # → notas.md
|
||||||
|
# Editar con Emacs y personalizar
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Estructura del Repositorio
|
## Formato del fichero notas.md
|
||||||
|
|
||||||
```
|
```markdown
|
||||||
~/napi/
|
# Notas — NombreAlumno
|
||||||
├── README.md ← este fichero
|
|
||||||
├── CLAUDE.md ← instrucciones para Claude Code
|
> 🏫 **Módulo:** NOMBRE_MODULO
|
||||||
├── viewer.html ← app web (fetch + marked.js + twemoji)
|
> 📅 **Última actualización:** YYYY-MM-DD
|
||||||
├── marked.min.js ← renderer Markdown local
|
|
||||||
├── nginx/
|
|
||||||
│ ├── README.md ← documentación configs Nginx
|
|
||||||
│ ├── napi-ddaw2.conf ← server block notas.qu3v3d0.tech
|
|
||||||
│ └── napi2-asir1.conf ← server block asir1.qu3v3d0.tech
|
|
||||||
├── scripts/
|
|
||||||
│ ├── README.md ← documentación watchers y servicios
|
|
||||||
│ ├── python-upload-watcher.sh ← watcher ASIR1 (v7)
|
|
||||||
│ ├── nginx-user-config-watcher.sh ← watcher DDAW2 (v3)
|
|
||||||
│ ├── xmpp-notify.py ← bot XMPP one-shot
|
|
||||||
│ ├── python-upload-watcher.service ← unit systemd
|
|
||||||
│ ├── nginx-user-config-watcher.service
|
|
||||||
│ ├── home-fenix-napi-data.mount ← sshfs DDAW2
|
|
||||||
│ └── home-fenix-napi-data2.mount ← sshfs ASIR1
|
|
||||||
└── Screenshots/
|
|
||||||
└── zzz@librebits.info.png ← ejemplo notificaciones XMPP
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Conexión a zzz
|
## 📊 Resumen
|
||||||
|
|
||||||
```bash
|
| Práctica | Título | Nota | Estado |
|
||||||
ssh fenix@qu3v3d0.tech # clave sin -i, fenix tiene sudo
|
|:---------|:-------|:----:|:------:|
|
||||||
```
|
| P2.3 | Nginx via SFTP | 7/10 | ✅ |
|
||||||
|
| P2.4 | HTTP y HTTPS | 8/10 | ✅ |
|
||||||
|
| P2.7 | Multi-sitio | 9/10 | ✅ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Gestión de Servicios
|
## P2.7 — Multi-sitio Web con Nginx
|
||||||
|
|
||||||
### Watchers (en zzz)
|
**Nota: 9/10** · _Escaneado: 2026-02-19_
|
||||||
|
|
||||||
```bash
|
NombreAlumno, [feedback personalizado].
|
||||||
sudo systemctl status python-upload-watcher.service # ASIR1
|
|
||||||
sudo systemctl status nginx-user-config-watcher.service # DDAW2
|
### Criterios
|
||||||
sudo systemctl restart python-upload-watcher.service
|
|
||||||
|
| Criterio | Puntos | Estado |
|
||||||
|
|:---------|:------:|:------:|
|
||||||
|
| Hextris desplegado + HTTPS | 1,5/1,5 | ✅ |
|
||||||
|
| Gzip | 1/1 | ✅ |
|
||||||
|
| CSP correcta | 1/1,5 | ⚠️ unsafe-inline |
|
||||||
|
|
||||||
|
### Próximos pasos
|
||||||
|
|
||||||
|
1. Corregir CSP: eliminar `unsafe-inline`
|
||||||
|
2. Subir capturas de verificación
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mounts sshfs (en aldebaran/anka4)
|
Ver `_plantilla/notas.md` como punto de partida para nuevos alumnos.
|
||||||
|
|
||||||
```bash
|
---
|
||||||
systemctl --user status home-fenix-napi\\x2ddata.mount # DDAW2
|
|
||||||
systemctl --user status home-fenix-napi\\x2ddata2.mount # ASIR1
|
## Ficheros de la app (en el servidor)
|
||||||
|
|
||||||
|
### `viewer.html`
|
||||||
|
|
||||||
|
App completa en ~40 líneas. Fetcha `/notas.md` (que Nginx resuelve al fichero
|
||||||
|
del alumno autenticado) y lo renderiza con `marked.min.js`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Notas</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
|
||||||
|
table { border-collapse: collapse; width: 100%; }
|
||||||
|
th, td { border: 1px solid #ccc; padding: 0.5rem 1rem; text-align: left; }
|
||||||
|
th { background: #f4f4f4; }
|
||||||
|
code { background: #f0f0f0; padding: 0.1em 0.4em; border-radius: 3px; }
|
||||||
|
pre code { display: block; padding: 1rem; overflow-x: auto; }
|
||||||
|
blockquote { border-left: 4px solid #ccc; margin: 0; padding-left: 1rem; color: #555; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="content">Cargando...</div>
|
||||||
|
<script src="marked.min.js"></script>
|
||||||
|
<script>
|
||||||
|
fetch('/notas.md')
|
||||||
|
.then(r => r.ok ? r.text() : Promise.reject(r.status))
|
||||||
|
.then(md => { document.getElementById('content').innerHTML = marked.parse(md); })
|
||||||
|
.catch(e => { document.getElementById('content').textContent = 'Error: ' + e; });
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Logs
|
### `marked.min.js`
|
||||||
|
|
||||||
|
Descargar de https://cdn.jsdelivr.net/npm/marked/marked.min.js y guardar como
|
||||||
|
fichero local en `/var/www/api/marked.min.js`. No referenciar CDN externo
|
||||||
|
(rompe la CSP y crea dependencia de terceros).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gestión del mount sshfs
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo tail -f /var/log/python-upload-watcher.log # ASIR1 watcher
|
# Estado
|
||||||
sudo tail -f /var/log/nginx/napi-error.log # Nginx errors
|
systemctl --user status 'home-TUUSUARIO-napi\x2ddata.mount'
|
||||||
|
|
||||||
|
# Montar
|
||||||
|
systemctl --user start 'home-TUUSUARIO-napi\x2ddata.mount'
|
||||||
|
|
||||||
|
# Desmontar
|
||||||
|
systemctl --user stop 'home-TUUSUARIO-napi\x2ddata.mount'
|
||||||
|
|
||||||
|
# Si se cuelga (mount zombie):
|
||||||
|
fusermount -uz ~/napi-data
|
||||||
|
systemctl --user start 'home-TUUSUARIO-napi\x2ddata.mount'
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
| Problema | Causa | Solución |
|
| Síntoma | Causa probable | Solución |
|
||||||
|:---------|:------|:---------|
|
|:--------|:---------------|:---------|
|
||||||
| `403 Forbidden` | www-data no en grupo shadow | `sudo usermod -aG shadow www-data && sudo systemctl restart nginx` |
|
| `403 Forbidden` al acceder | www-data no está en grupo shadow | `sudo usermod -aG shadow www-data && sudo systemctl restart nginx` |
|
||||||
| `401 Unauthorized` | Credenciales incorrectas o PAM mal configurado | Verificar `/etc/pam.d/common-auth` |
|
| `401 Unauthorized` con credenciales correctas | PAM no configurado | Verificar que `auth_pam_service_name "common-auth"` existe en `/etc/pam.d/` |
|
||||||
| `twemoji is not defined` | Falta `twemoji.min.js` en el root del site | `sudo cp /var/www/napi/twemoji.min.js /var/www/napiN/` |
|
| `/notas.md` devuelve 404 | Carpeta del alumno no existe en `data/` | `sudo mkdir -p /var/www/api/data/$ALUMNO` |
|
||||||
| `404` en `/notas.md` | Carpeta del alumno no existe en `data/` | Crear carpeta + copiar plantilla |
|
| sshfs mount desaparece | Pérdida de conexión SSH | La opción `reconnect` lo recupera solo; si no, `systemctl --user restart` |
|
||||||
| sshfs zombie | Conexión SSH caída | `fusermount -uz ~/napi-dataN && systemctl --user restart ...mount` |
|
| El alumno ve las notas de otro | `$remote_user` vacío o PAM no activo | Verificar que `auth_pam` y `auth_pam_service_name` están en el bloque correcto |
|
||||||
| Watcher no detecta re-entregas | Versión antigua del watcher | Actualizar a v7+ y reiniciar servicio |
|
| `marked is not defined` | `marked.min.js` no accesible | Verificar que el fichero existe en `/var/www/api/` y tiene permisos de lectura |
|
||||||
| `__pycache__` en notificaciones | Watcher < v6 | Actualizar a v7+ |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Seguridad
|
## Seguridad
|
||||||
|
|
||||||
- Cada alumno solo ve **sus propias notas** — Nginx resuelve el path con `$remote_user`
|
- Cada alumno solo ve sus propias notas — Nginx resuelve el path con `$remote_user`
|
||||||
- Sin ejecución de código en el servidor — todo es estático
|
- Sin ejecución de código en el servidor — todo es estático
|
||||||
- `marked.min.js` y `twemoji.min.js` locales — sin dependencia de CDNs externos
|
- `marked.min.js` local — sin dependencia de CDNs externos
|
||||||
- Credenciales: las mismas que usa el alumno para SFTP (FileZilla)
|
- Credenciales: las mismas que usa el alumno para subir archivos por SFTP
|
||||||
- SSL/TLS obligatorio (redirección 301 desde HTTP)
|
- SSL/TLS obligatorio (redirección 301 desde HTTP)
|
||||||
- SFTP con chroot — alumnos no pueden ver otros directorios
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -352,7 +421,5 @@ sudo tail -f /var/log/nginx/napi-error.log # Nginx errors
|
||||||
|
|
||||||
| Fecha | Hito |
|
| Fecha | Hito |
|
||||||
|:------|:-----|
|
|:------|:-----|
|
||||||
| 2026-02-22 | MVP desplegado: notas.qu3v3d0.tech para DDAW2 (19 alumnos) |
|
| 2026-02-22 | MVP desplegado: sshfs + Nginx auth_pam + viewer.html + marked.js |
|
||||||
| 2026-02-25 | ASIR1 desplegado: asir1.qu3v3d0.tech para Programación (21 alumnos) |
|
| 2026-02-22 | notas.md generadas para los 19 alumnos de DDAW2 |
|
||||||
| 2026-02-25 | Watcher ASIR1 v7: batching, re-entregas, Windows-safe, __pycache__ filter, nombres completos |
|
|
||||||
| 2027-02-04 | Renovar certificado SSL wildcard (caduca ~365 días desde 2026-02-04) |
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 201 KiB |
|
|
@ -1,51 +0,0 @@
|
||||||
# 🌐 Configuraciones Nginx — napi
|
|
||||||
|
|
||||||
Copia de referencia de los server blocks desplegados en **zzz** (`qu3v3d0.tech`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Sites activos
|
|
||||||
|
|
||||||
| Fichero | server_name | Root | Grupo |
|
|
||||||
|:--------|:------------|:-----|:------|
|
|
||||||
| **`napi-ddaw2.conf`** | `notas.qu3v3d0.tech` | `/var/www/napi` | DDAW2 (19 alumnos) |
|
|
||||||
| **`napi2-asir1.conf`** | `asir1.qu3v3d0.tech` | `/var/www/napi2` | ASIR1 Programación (21 alumnos) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔑 Funcionamiento común
|
|
||||||
|
|
||||||
Ambos sites comparten la misma arquitectura:
|
|
||||||
|
|
||||||
1. **Auth PAM** (`libnginx-mod-http-auth-pam`) — credenciales SFTP del alumno
|
|
||||||
2. **`$remote_user`** — Nginx usa el usuario autenticado para servir su `notas.md`
|
|
||||||
3. **`viewer.html`** — App estática que hace `fetch('/notas.md')` y renderiza con `marked.js` + `twemoji`
|
|
||||||
4. **SSL wildcard** — `*.qu3v3d0.tech` (certificado en `/etc/ssl/certs/qu3v3d0.tech.crt`)
|
|
||||||
|
|
||||||
```
|
|
||||||
Alumno → https://asir1.qu3v3d0.tech → Auth PAM → viewer.html
|
|
||||||
→ fetch('/notas.md') → Nginx alias → /var/www/napi2/data/$remote_user/notas.md
|
|
||||||
→ marked.js renderiza → alumno ve su feedback
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Gestión en zzz
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ver configs activos
|
|
||||||
ls -la /etc/nginx/sites-enabled/
|
|
||||||
|
|
||||||
# Editar
|
|
||||||
sudo nano /etc/nginx/sites-available/napi2
|
|
||||||
|
|
||||||
# Test + reload
|
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📅 DNS
|
|
||||||
|
|
||||||
DNS wildcard `*.qu3v3d0.tech` → `161.22.44.104` (zzz).
|
|
||||||
No hay que tocar DNS para añadir nuevos subdominios.
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
# napi — Notas API (notas.qu3v3d0.tech)
|
|
||||||
# Auth: PAM (mismas credenciales que SFTP)
|
|
||||||
# Datos: /var/www/api/data/$remote_user/notas.md
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name notas.qu3v3d0.tech;
|
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name notas.qu3v3d0.tech;
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/certs/qu3v3d0.tech.crt;
|
|
||||||
ssl_certificate_key /etc/ssl/private/qu3v3d0.tech.key;
|
|
||||||
|
|
||||||
root /var/www/api;
|
|
||||||
|
|
||||||
auth_pam "Notas DDAW2";
|
|
||||||
auth_pam_service_name "common-auth";
|
|
||||||
|
|
||||||
location = / {
|
|
||||||
try_files /viewer.html =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /notas.md {
|
|
||||||
alias /var/www/api/data/$remote_user/notas.md;
|
|
||||||
default_type text/plain;
|
|
||||||
charset utf-8;
|
|
||||||
add_header Cache-Control "no-cache";
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.(js|css)$ {
|
|
||||||
expires 7d;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
# napi2 — Notas Programación ASIR1 (asir1.qu3v3d0.tech)
|
|
||||||
# Auth: PAM (mismas credenciales que SFTP)
|
|
||||||
# Datos: /var/www/napi2/data/$remote_user/notas.md
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name asir1.qu3v3d0.tech;
|
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name asir1.qu3v3d0.tech;
|
|
||||||
|
|
||||||
ssl_certificate /etc/ssl/certs/qu3v3d0.tech.crt;
|
|
||||||
ssl_certificate_key /etc/ssl/private/qu3v3d0.tech.key;
|
|
||||||
|
|
||||||
root /var/www/napi2;
|
|
||||||
|
|
||||||
auth_pam "Notas Programacion ASIR1";
|
|
||||||
auth_pam_service_name "common-auth";
|
|
||||||
|
|
||||||
location = / {
|
|
||||||
try_files /viewer.html =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location = /notas.md {
|
|
||||||
alias /var/www/napi2/data/$remote_user/notas.md;
|
|
||||||
default_type text/plain;
|
|
||||||
charset utf-8;
|
|
||||||
add_header Cache-Control "no-cache";
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.(js|css)$ {
|
|
||||||
expires 7d;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
return 404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
# 📜 Scripts y Servicios — napi
|
|
||||||
|
|
||||||
Copia de referencia de todos los scripts y unidades systemd desplegados en **zzz** (`qu3v3d0.tech`) y **aldebaran/anka4** para el sistema de retroalimentación a estudiantes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🗂️ Inventario
|
|
||||||
|
|
||||||
| Fichero | Dónde vive | Descripción |
|
|
||||||
|:--------|:-----------|:------------|
|
|
||||||
| **`python-upload-watcher.sh`** | `zzz:/usr/local/bin/` | Watcher ASIR1 — detecta entregas SFTP en `/home/USER/python/`, agrupa por carpeta, notifica XMPP |
|
|
||||||
| **`nginx-user-config-watcher.sh`** | `zzz:/usr/local/bin/` | Watcher DDAW2 — valida y despliega `*.conf` de alumnos, analiza CSP, notifica XMPP |
|
|
||||||
| **`xmpp-notify.py`** | `zzz:/usr/local/bin/` | Bot XMPP one-shot (slixmpp) — envía mensaje a `jla@librebits.info` |
|
|
||||||
| **`python-upload-watcher.service`** | `zzz:/etc/systemd/system/` | Unit systemd para el watcher ASIR1 |
|
|
||||||
| **`nginx-user-config-watcher.service`** | `zzz:/etc/systemd/system/` | Unit systemd para el watcher DDAW2 |
|
|
||||||
| **`home-fenix-napi-data.mount`** | `aldebaran:~/.config/systemd/user/` | Mount sshfs `~/napi-data/` → `zzz:/var/www/napi/data/` (DDAW2) |
|
|
||||||
| **`home-fenix-napi-data2.mount`** | `aldebaran:~/.config/systemd/user/` | Mount sshfs `~/napi-data2/` → `zzz:/var/www/napi2/data/` (ASIR1) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🐍 python-upload-watcher.sh (v7)
|
|
||||||
|
|
||||||
**Propósito:** Monitorizar las entregas de prácticas de Programación (ASIR1) subidas por SFTP.
|
|
||||||
|
|
||||||
### Características
|
|
||||||
|
|
||||||
- ✅ **Batching por carpeta** — Cuando un alumno sube `PRACTICA3.1/` con N ficheros, envía **un solo mensaje XMPP** con el listado completo (espera 10s de silencio)
|
|
||||||
- ♻️ **Re-entregas** — Si el alumno borra y re-sube la misma carpeta (<120s), etiqueta como "Re-entrega"
|
|
||||||
- 👤 **Nombre completo** — Notifica `[María Jara]` en vez de `[jara]` (mapa usuario → nombre en el script)
|
|
||||||
- 🪟 **Windows-safe** — Ignora `Thumbs.db`, `desktop.ini`, `.DS_Store`, `*.tmp`, `~$*`
|
|
||||||
- 🐍 **Python-safe** — Ignora `__pycache__/`, `*.pyc`, `*.pyo`
|
|
||||||
- 🛠️ **Dev-safe** — Ignora `.git/`, `.venv/`, `node_modules/`, `.idea/`, `.vscode/`
|
|
||||||
- 📁 **Iconos por tipo** — 🐍 `.py` · 📦 `.zip/.rar` · 📝 `.md` · 📄 `.txt` · 📕 `.pdf` · 📘 `.docx`
|
|
||||||
|
|
||||||
### Ejemplo de notificación XMPP
|
|
||||||
|
|
||||||
```
|
|
||||||
📁 [Andrés Barrios] Entrega ASIR1: PRACTICA3.1/ (4 ficheros)
|
|
||||||
🐍 main.py (2048 bytes)
|
|
||||||
🐍 utils.py (1024 bytes)
|
|
||||||
📝 README.md (512 bytes)
|
|
||||||
📄 requisitos.txt (128 bytes)
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
📁 [Andrés Barrios] ♻️ Re-entrega ASIR1: PRACTICA3.1/ (3 ficheros)
|
|
||||||
🐍 main.py (2100 bytes)
|
|
||||||
📝 README.md (600 bytes)
|
|
||||||
📄 requisitos.txt (128 bytes)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Eventos inotifywait monitorizados
|
|
||||||
|
|
||||||
| Evento | Acción |
|
|
||||||
|:-------|:-------|
|
|
||||||
| `CREATE` (directorio) | Abre batch, detecta re-entrega si fue borrado recientemente |
|
|
||||||
| `CREATE` (fichero) | Ignorado (esperamos `close_write`) |
|
|
||||||
| `CLOSE_WRITE` | Fichero completado → añade al batch o notifica individual |
|
|
||||||
| `MOVED_TO` | Fichero movido al directorio → igual que `close_write` |
|
|
||||||
| `DELETE` / `MOVED_FROM` | Registra borrado de carpeta (para detectar re-entregas) |
|
|
||||||
|
|
||||||
### Log
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tail -f /var/log/python-upload-watcher.log # en zzz
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌐 nginx-user-config-watcher.sh (v3)
|
|
||||||
|
|
||||||
**Propósito:** Despliegue automático de configuraciones Nginx subidas por alumnos de DDAW2 via SFTP.
|
|
||||||
|
|
||||||
### Características
|
|
||||||
|
|
||||||
- ✅ **Validación de seguridad** — Verifica `server_name`, `root`, bloquea directivas peligrosas (`proxy_pass`, `include /`, etc.)
|
|
||||||
- 🔒 **Análisis CSP** — Revisa cabeceras `Content-Security-Policy`, detecta `unsafe-inline`, multiline, HTTPS
|
|
||||||
- 📛 **Nomenclatura** — Verifica patrón `mi-nginx[-SUFIJO].conf`
|
|
||||||
- 🌐 **Multi-sitio v3** — `mi-nginx-hextris.conf` → `USER-hextris.qu3v3d0.tech`
|
|
||||||
- 🗑️ **Undeploy** — Borrar el `.conf` via SFTP elimina el site de Nginx
|
|
||||||
- 💡 **Pistas humanizadas** — Traduce errores `nginx -t` a español comprensible
|
|
||||||
|
|
||||||
### Límites
|
|
||||||
|
|
||||||
- Máximo **4 sitios** por alumno
|
|
||||||
- Solo `.conf` dentro de `/home/USER/html/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📨 xmpp-notify.py
|
|
||||||
|
|
||||||
Bot XMPP one-shot usando `slixmpp`. Envía un mensaje y desconecta.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/usr/local/bin/xmpp-notify.py "Mensaje de prueba"
|
|
||||||
```
|
|
||||||
|
|
||||||
- **JID:** `zzz@librebits.info`
|
|
||||||
- **Destinatario:** `jla@librebits.info`
|
|
||||||
- **Config:** `/etc/xmpp-notify.conf` (credenciales)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Units systemd
|
|
||||||
|
|
||||||
### En zzz (system-level)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Estado de los watchers
|
|
||||||
sudo systemctl status python-upload-watcher.service # ASIR1
|
|
||||||
sudo systemctl status nginx-user-config-watcher.service # DDAW2
|
|
||||||
|
|
||||||
# Reiniciar
|
|
||||||
sudo systemctl restart python-upload-watcher.service
|
|
||||||
sudo systemctl restart nginx-user-config-watcher.service
|
|
||||||
```
|
|
||||||
|
|
||||||
### En aldebaran/anka4 (user-level)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Estado de los mounts sshfs
|
|
||||||
systemctl --user status home-fenix-napi\\x2ddata.mount # DDAW2
|
|
||||||
systemctl --user status home-fenix-napi\\x2ddata2.mount # ASIR1
|
|
||||||
|
|
||||||
# Montar/desmontar
|
|
||||||
systemctl --user start home-fenix-napi\\x2ddata2.mount
|
|
||||||
systemctl --user stop home-fenix-napi\\x2ddata2.mount
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🏗️ Estructura de directorios en zzz
|
|
||||||
|
|
||||||
```
|
|
||||||
/var/www/napi/ ← DDAW2
|
|
||||||
├── viewer.html
|
|
||||||
├── marked.min.js
|
|
||||||
├── twemoji.min.js
|
|
||||||
└── data/
|
|
||||||
├── anas/notas.md
|
|
||||||
├── pablo/notas.md
|
|
||||||
└── ... (19 alumnos)
|
|
||||||
|
|
||||||
/var/www/napi2/ ← ASIR1 (Programación)
|
|
||||||
├── viewer.html
|
|
||||||
├── marked.min.js
|
|
||||||
├── twemoji.min.js
|
|
||||||
└── data/
|
|
||||||
├── barja/notas.md
|
|
||||||
├── barrios/notas.md
|
|
||||||
└── ... (21 alumnos)
|
|
||||||
|
|
||||||
/home/USER/python/ ← Entregas SFTP (ASIR1)
|
|
||||||
├── PRACTICA3.1/
|
|
||||||
│ ├── main.py
|
|
||||||
│ └── README.md
|
|
||||||
└── fichero_suelto.py
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📅 Historial de versiones
|
|
||||||
|
|
||||||
| Script | Versión | Fecha | Cambios |
|
|
||||||
|:-------|:--------|:------|:--------|
|
|
||||||
| `python-upload-watcher.sh` | **v7** | 2026-02-25 | Batching + re-entregas + Windows-safe + `__pycache__` filter + nombres completos |
|
|
||||||
| `nginx-user-config-watcher.sh` | **v3** | 2026-02-19 | Multi-sitio + undeploy + CSP analysis |
|
|
||||||
| `xmpp-notify.py` | **v1** | 2026-01-27 | Bot one-shot slixmpp |
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=SSHFS mount napi-data from zzz (qu3v3d0.tech)
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Mount]
|
|
||||||
What=fenix@qu3v3d0.tech:/var/www/napi/data
|
|
||||||
Where=/home/fenix/napi-data
|
|
||||||
Type=fuse.sshfs
|
|
||||||
Options=reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,IdentityFile=/home/fenix/.ssh/id_rsa,_netdev,allow_other
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=SSHFS mount napi-data2 from zzz (qu3v3d0.tech) — Programación ASIR1
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Mount]
|
|
||||||
What=fenix@qu3v3d0.tech:/var/www/napi2/data
|
|
||||||
Where=/home/fenix/napi-data2
|
|
||||||
Type=fuse.sshfs
|
|
||||||
Options=reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,IdentityFile=/home/fenix/.ssh/id_rsa,_netdev,allow_other
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=default.target
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Watch and deploy user Nginx configs
|
|
||||||
After=network.target nginx.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/usr/local/bin/nginx-user-config-watcher.sh
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
@ -1,508 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# /usr/local/bin/nginx-user-config-watcher.sh
|
|
||||||
# Watcher para configs nginx de usuarios via SFTP
|
|
||||||
# Detecta cualquier archivo *.conf en /home/USER/html/
|
|
||||||
# Con notificaciones XMPP a jla@librebits.info
|
|
||||||
#
|
|
||||||
# v2 — Análisis CSP post-deploy + humanización de errores nginx (P2.5)
|
|
||||||
# v3 — Multi-sitio por alumno: mi-nginx-SUFIJO.conf → USER-SUFIJO.qu3v3d0.tech
|
|
||||||
# Retrocompatible: mi-nginx.conf (sin sufijo) funciona igual que v2
|
|
||||||
|
|
||||||
WATCH_DIR="/home"
|
|
||||||
LOG="/var/log/nginx-user-configs.log"
|
|
||||||
MAX_SITES=4
|
|
||||||
|
|
||||||
log() {
|
|
||||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Notificación XMPP (fire and forget)
|
|
||||||
notify_xmpp() {
|
|
||||||
local msg="$1"
|
|
||||||
/usr/local/bin/xmpp-notify.py "$msg" >/dev/null 2>&1 &
|
|
||||||
}
|
|
||||||
|
|
||||||
# -------------------------------------------------------
|
|
||||||
# analyze_csp() — Analiza un .conf desplegado buscando
|
|
||||||
# cabeceras CSP y de seguridad. Devuelve texto con pistas.
|
|
||||||
# -------------------------------------------------------
|
|
||||||
analyze_csp() {
|
|
||||||
local conf="$1"
|
|
||||||
local hints=""
|
|
||||||
local conf_content
|
|
||||||
conf_content=$(cat "$conf" 2>/dev/null) || return
|
|
||||||
|
|
||||||
# --- Check 1: ¿Tiene cabecera CSP? ---
|
|
||||||
if ! echo "$conf_content" | grep -q "Content-Security-Policy"; then
|
|
||||||
hints="${hints}💡 Tu .conf no tiene cabecera Content-Security-Policy (opcional según la práctica)
|
|
||||||
"
|
|
||||||
else
|
|
||||||
# --- Check 2: ¿CSP multiline? (add_header ... " sin cierre en misma línea) ---
|
|
||||||
# Buscamos líneas con add_header Content-Security-Policy que abren comillas
|
|
||||||
# pero no las cierran en la misma línea
|
|
||||||
if echo "$conf_content" | grep -E 'add_header\s+Content-Security-Policy' | grep -qE '"[^"]*$'; then
|
|
||||||
hints="${hints}⚠️ CSP con saltos de línea: ponlo todo en UNA sola línea entre comillas
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 3: ¿CSP en bloque HTTPS (443) o solo en HTTP (80)? ---
|
|
||||||
# Estrategia: buscar si CSP aparece ANTES del primer listen 443
|
|
||||||
# o si no hay listen 443 en absoluto
|
|
||||||
local has_443
|
|
||||||
has_443=$(echo "$conf_content" | grep -c "listen.*443")
|
|
||||||
if [[ "$has_443" -eq 0 ]]; then
|
|
||||||
hints="${hints}⚠️ No hay bloque HTTPS (listen 443) — la CSP debería ir en el bloque HTTPS
|
|
||||||
"
|
|
||||||
else
|
|
||||||
# Comprobar si CSP está solo en bloque HTTP (antes de listen 443)
|
|
||||||
# Obtenemos la línea de CSP y la línea de listen 443
|
|
||||||
local csp_line
|
|
||||||
local ssl_line
|
|
||||||
csp_line=$(echo "$conf_content" | grep -n "Content-Security-Policy" | head -1 | cut -d: -f1)
|
|
||||||
ssl_line=$(echo "$conf_content" | grep -n "listen.*443" | head -1 | cut -d: -f1)
|
|
||||||
if [[ -n "$csp_line" && -n "$ssl_line" && "$csp_line" -lt "$ssl_line" ]]; then
|
|
||||||
# CSP aparece antes del bloque 443 — verificar si también está después
|
|
||||||
local csp_after_ssl
|
|
||||||
csp_after_ssl=$(echo "$conf_content" | tail -n +"$ssl_line" | grep -c "Content-Security-Policy")
|
|
||||||
if [[ "$csp_after_ssl" -eq 0 ]]; then
|
|
||||||
hints="${hints}⚠️ CSP solo en bloque HTTP (80), no en HTTPS (443) — ponla en el bloque donde se sirve tu web
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 4: ¿unsafe-inline en script-src? ---
|
|
||||||
if echo "$conf_content" | grep -i "script-src" | grep -qi "unsafe-inline"; then
|
|
||||||
hints="${hints}⚠️ unsafe-inline en script-src debilita la protección CSP contra XSS
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 5: ¿Falta 'always' en add_header CSP? ---
|
|
||||||
if echo "$conf_content" | grep "Content-Security-Policy" | grep -qv "always"; then
|
|
||||||
hints="${hints}💡 Consejo: añade 'always' para que CSP se envíe también en respuestas 4xx/5xx
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- Check 6: ¿Tiene las otras cabeceras de seguridad? ---
|
|
||||||
local missing_headers=""
|
|
||||||
if ! echo "$conf_content" | grep -q "X-Content-Type-Options"; then
|
|
||||||
missing_headers="${missing_headers} X-Content-Type-Options"
|
|
||||||
fi
|
|
||||||
if ! echo "$conf_content" | grep -q "X-Frame-Options"; then
|
|
||||||
missing_headers="${missing_headers} X-Frame-Options"
|
|
||||||
fi
|
|
||||||
if ! echo "$conf_content" | grep -q "X-XSS-Protection"; then
|
|
||||||
missing_headers="${missing_headers} X-XSS-Protection"
|
|
||||||
fi
|
|
||||||
if ! echo "$conf_content" | grep -q "Referrer-Policy"; then
|
|
||||||
missing_headers="${missing_headers} Referrer-Policy"
|
|
||||||
fi
|
|
||||||
if [[ -n "$missing_headers" ]]; then
|
|
||||||
hints="${hints}💡 Faltan cabeceras de seguridad:${missing_headers}
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Devolver resultado
|
|
||||||
if [[ -z "$hints" ]]; then
|
|
||||||
echo "🔒 CSP y cabeceras de seguridad: todo OK"
|
|
||||||
else
|
|
||||||
echo "$hints"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# -------------------------------------------------------
|
|
||||||
# humanize_nginx_error() — Traduce errores nginx -t
|
|
||||||
# a pistas comprensibles para el alumno.
|
|
||||||
# -------------------------------------------------------
|
|
||||||
humanize_nginx_error() {
|
|
||||||
local error_output="$1"
|
|
||||||
local pistas=""
|
|
||||||
|
|
||||||
if echo "$error_output" | grep -q 'unexpected "}"'; then
|
|
||||||
pistas="${pistas}💡 ¿Te falta un ';' al final de alguna directiva?
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
if echo "$error_output" | grep -q "unexpected end of file"; then
|
|
||||||
pistas="${pistas}💡 ¿Cerraste todos los bloques server { } con '}'?
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
if echo "$error_output" | grep -q "directive is not allowed"; then
|
|
||||||
pistas="${pistas}💡 Revisa que cada directiva esté dentro del bloque server { } correcto
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
if echo "$error_output" | grep -q "unknown directive"; then
|
|
||||||
pistas="${pistas}💡 ¿Hay algún typo en el nombre de una directiva?
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
if echo "$error_output" | grep -q "invalid number of arguments"; then
|
|
||||||
pistas="${pistas}💡 Alguna directiva tiene argumentos de más o de menos — revisa comillas y punto y coma
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
if echo "$error_output" | grep -qE '(missing|unexpected) ";"'; then
|
|
||||||
pistas="${pistas}💡 Revisa los punto y coma (;) — puede que sobre o falte alguno
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "$pistas"
|
|
||||||
}
|
|
||||||
|
|
||||||
# -------------------------------------------------------
|
|
||||||
# check_https_redirect() — Verifica si la config tiene
|
|
||||||
# redirección HTTP → HTTPS (return 301 https://)
|
|
||||||
# -------------------------------------------------------
|
|
||||||
check_https_redirect() {
|
|
||||||
local conf="$1"
|
|
||||||
local expected_name="$2"
|
|
||||||
local conf_content
|
|
||||||
conf_content=$(cat "$conf" 2>/dev/null) || return
|
|
||||||
local hints=""
|
|
||||||
|
|
||||||
# ¿Tiene listen 443 ssl? (es decir, ¿sirve HTTPS?)
|
|
||||||
if echo "$conf_content" | grep -q "listen.*443.*ssl"; then
|
|
||||||
# Tiene HTTPS → ¿redirige HTTP a HTTPS?
|
|
||||||
if ! echo "$conf_content" | grep -qE "return\s+301\s+https://"; then
|
|
||||||
hints="${hints}⚠️ HTTPS activo pero NO hay redirección HTTP → HTTPS.
|
|
||||||
💡 Añade un bloque server { listen 80; return 301 https://\$server_name\$request_uri; }
|
|
||||||
"
|
|
||||||
else
|
|
||||||
hints="${hints}✅ Redirección HTTP → HTTPS configurada correctamente
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# No tiene HTTPS
|
|
||||||
hints="${hints}⚠️ Tu config no tiene bloque HTTPS (listen 443 ssl) — el tráfico no va cifrado
|
|
||||||
💡 Recuerda: la práctica requiere forzar tráfico HTTP → HTTPS
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "$hints"
|
|
||||||
}
|
|
||||||
|
|
||||||
# -------------------------------------------------------
|
|
||||||
# check_naming_convention() — Avisa si el fichero .conf
|
|
||||||
# no sigue la nomenclatura mi-nginx[-SUFIJO].conf
|
|
||||||
# -------------------------------------------------------
|
|
||||||
check_naming_convention() {
|
|
||||||
local conffile="$1"
|
|
||||||
if [[ ! "$conffile" =~ ^mi-nginx(-[a-z0-9]+)?\.conf$ ]]; then
|
|
||||||
echo "📛 Tu fichero se llama '$conffile' — la nomenclatura recomendada es:
|
|
||||||
• mi-nginx.conf → sitio principal
|
|
||||||
• mi-nginx-hextris.conf → segundo sitio (Hextris)
|
|
||||||
• mi-nginx-app.conf → tercer sitio (App)
|
|
||||||
Usa el formato mi-nginx-NOMBRE.conf para que el sistema detecte automáticamente tu subdominio.
|
|
||||||
"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# -------------------------------------------------------
|
|
||||||
# extract_app_suffix() — Extrae el sufijo de app del nombre del .conf
|
|
||||||
# mi-nginx.conf → "" (vacío = sitio principal)
|
|
||||||
# mi-nginx-portfolio.conf → "portfolio"
|
|
||||||
# otro-nombre.conf → "" (no sigue el patrón multi-sitio)
|
|
||||||
# -------------------------------------------------------
|
|
||||||
extract_app_suffix() {
|
|
||||||
local conffile="$1"
|
|
||||||
if [[ "$conffile" =~ ^mi-nginx-([a-z0-9]+)\.conf$ ]]; then
|
|
||||||
echo "${BASH_REMATCH[1]}"
|
|
||||||
else
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# -------------------------------------------------------
|
|
||||||
# undeploy_site() — Elimina un site de nginx cuando el
|
|
||||||
# alumno borra su mi-nginx[-SUFIJO].conf via SFTP
|
|
||||||
# -------------------------------------------------------
|
|
||||||
undeploy_site() {
|
|
||||||
local user="$1"
|
|
||||||
local conffile="$2"
|
|
||||||
|
|
||||||
local app_suffix
|
|
||||||
app_suffix=$(extract_app_suffix "$conffile")
|
|
||||||
|
|
||||||
local site_id
|
|
||||||
if [[ -n "$app_suffix" ]]; then
|
|
||||||
site_id="${user}-${app_suffix}"
|
|
||||||
else
|
|
||||||
site_id="${user}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local dest="/etc/nginx/sites-available/$site_id"
|
|
||||||
local enabled="/etc/nginx/sites-enabled/$site_id"
|
|
||||||
|
|
||||||
# Solo actuar si el site existe desplegado
|
|
||||||
if [[ -f "$dest" || -L "$enabled" ]]; then
|
|
||||||
rm -f "$enabled" "$dest"
|
|
||||||
|
|
||||||
# Validar que nginx sigue OK sin ese site
|
|
||||||
if nginx -t 2>&1 | grep -q "successful"; then
|
|
||||||
systemctl reload nginx
|
|
||||||
log "[$user] UNDEPLOY: Site $site_id eliminado (borrado $conffile) y nginx recargado"
|
|
||||||
notify_xmpp "🗑️ [$user] Site $site_id eliminado (borrado $conffile)"
|
|
||||||
else
|
|
||||||
log "[$user] UNDEPLOY: Site $site_id eliminado pero nginx -t falló (recargando de todas formas)"
|
|
||||||
systemctl reload nginx
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_and_deploy() {
|
|
||||||
local user="$1"
|
|
||||||
local conffile="$2"
|
|
||||||
local src="/home/$user/html/$conffile"
|
|
||||||
|
|
||||||
# --- v3: Determinar sufijo de app y nombres esperados ---
|
|
||||||
local app_suffix
|
|
||||||
app_suffix=$(extract_app_suffix "$conffile")
|
|
||||||
|
|
||||||
local expected_name
|
|
||||||
local expected_root
|
|
||||||
local site_id
|
|
||||||
|
|
||||||
if [[ -n "$app_suffix" ]]; then
|
|
||||||
expected_name="${user}-${app_suffix}"
|
|
||||||
expected_root="/home/$user/html/$app_suffix"
|
|
||||||
site_id="${user}-${app_suffix}"
|
|
||||||
else
|
|
||||||
expected_name="${user}"
|
|
||||||
expected_root="/home/$user/html"
|
|
||||||
site_id="${user}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
local dest="/etc/nginx/sites-available/$site_id"
|
|
||||||
local enabled="/etc/nginx/sites-enabled/$site_id"
|
|
||||||
local tmp="/tmp/nginx-test-${site_id}.conf"
|
|
||||||
local backup="/tmp/nginx-backup-${site_id}.conf"
|
|
||||||
local error_file="/home/$user/html/nginx-error.log"
|
|
||||||
local status_file="/home/$user/html/nginx-status.log"
|
|
||||||
local practica_file="/home/$user/html/practica-status.log"
|
|
||||||
|
|
||||||
# Verificar que el archivo existe
|
|
||||||
[[ ! -f "$src" ]] && return 1
|
|
||||||
|
|
||||||
# --- v3: Límite de sitios por alumno (solo para sitios NUEVOS) ---
|
|
||||||
if [[ -n "$app_suffix" && ! -f "$dest" ]]; then
|
|
||||||
local current_sites
|
|
||||||
current_sites=$(ls /etc/nginx/sites-enabled/${user} /etc/nginx/sites-enabled/${user}-* 2>/dev/null | wc -l)
|
|
||||||
if (( current_sites >= MAX_SITES )); then
|
|
||||||
log "[$user] RECHAZADO: límite de $MAX_SITES sitios alcanzado"
|
|
||||||
echo "ERROR: Ya tienes $MAX_SITES sitios desplegados (límite máximo $MAX_SITES)." > "$error_file"
|
|
||||||
echo "Elimina algún mi-nginx-*.conf si quieres crear uno nuevo." >> "$error_file"
|
|
||||||
chown $user:www-data "$error_file" 2>/dev/null
|
|
||||||
notify_xmpp "❌ [$user] Config RECHAZADA: límite de $MAX_SITES sitios alcanzado"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copiar a temporal para validar
|
|
||||||
cp "$src" "$tmp"
|
|
||||||
|
|
||||||
# Comprobar nomenclatura del fichero (aviso temprano)
|
|
||||||
local naming_check
|
|
||||||
naming_check=$(check_naming_convention "$conffile")
|
|
||||||
|
|
||||||
# SEGURIDAD: Verificar que server_name coincide con el esperado
|
|
||||||
if ! grep -qE "^\s*server_name\s+${expected_name}\.(local|qu3v3d0\.tech)" "$tmp"; then
|
|
||||||
log "[$user] RECHAZADO: server_name no coincide (esperado: ${expected_name}.qu3v3d0.tech)"
|
|
||||||
{
|
|
||||||
echo "ERROR: server_name debe ser ${expected_name}.qu3v3d0.tech"
|
|
||||||
if [[ -n "$naming_check" ]]; then
|
|
||||||
echo ""
|
|
||||||
echo "$naming_check"
|
|
||||||
fi
|
|
||||||
} > "$error_file"
|
|
||||||
if [[ -n "$app_suffix" ]]; then
|
|
||||||
echo "(Para el fichero $conffile el subdominio esperado es ${expected_name}.qu3v3d0.tech)" >> "$error_file"
|
|
||||||
fi
|
|
||||||
chown $user:www-data "$error_file" 2>/dev/null
|
|
||||||
notify_xmpp "❌ [$user] Config RECHAZADA: server_name incorrecto ($conffile, esperado: ${expected_name})"
|
|
||||||
rm -f "$tmp"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# SEGURIDAD: Verificar que root apunta al directorio correcto
|
|
||||||
# - Sin sufijo: prefijo /home/USER/html (v2-compatible, permite subdirectorios)
|
|
||||||
# - Con sufijo: exacto /home/USER/html/SUFIJO (multi-sitio v3)
|
|
||||||
if [[ -n "$app_suffix" ]]; then
|
|
||||||
if ! grep -qE "^\s*root\s+${expected_root}\s*;" "$tmp"; then
|
|
||||||
log "[$user] RECHAZADO: root no apunta a $expected_root"
|
|
||||||
echo "ERROR: root debe ser ${expected_root}" > "$error_file"
|
|
||||||
echo "(Para el fichero $conffile la carpeta debe ser html/$app_suffix/)" >> "$error_file"
|
|
||||||
chown $user:www-data "$error_file" 2>/dev/null
|
|
||||||
notify_xmpp "❌ [$user] Config RECHAZADA: root incorrecto ($conffile, esperado: ${expected_root})"
|
|
||||||
rm -f "$tmp"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Retrocompatible con v2: acepta /home/USER/html y cualquier subdirectorio
|
|
||||||
if ! grep -qE "^\s*root\s+/home/$user/html" "$tmp"; then
|
|
||||||
log "[$user] RECHAZADO: root no apunta a /home/$user/html"
|
|
||||||
echo "ERROR: root debe ser /home/$user/html" > "$error_file"
|
|
||||||
chown $user:www-data "$error_file" 2>/dev/null
|
|
||||||
notify_xmpp "❌ [$user] Config RECHAZADA: root incorrecto ($conffile)"
|
|
||||||
rm -f "$tmp"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- v3: Verificar que el subdirectorio existe (solo multi-sitio) ---
|
|
||||||
if [[ -n "$app_suffix" && ! -d "$expected_root" ]]; then
|
|
||||||
log "[$user] RECHAZADO: directorio $expected_root no existe"
|
|
||||||
echo "ERROR: La carpeta '$app_suffix/' no existe dentro de html/." > "$error_file"
|
|
||||||
echo "Crea primero la carpeta via SFTP (FileZilla) y luego vuelve a subir $conffile." >> "$error_file"
|
|
||||||
chown $user:www-data "$error_file" 2>/dev/null
|
|
||||||
notify_xmpp "❌ [$user] Config RECHAZADA: carpeta html/$app_suffix/ no existe"
|
|
||||||
rm -f "$tmp"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# SEGURIDAD: Bloquear directivas peligrosas (ignorando comentarios)
|
|
||||||
if grep -v "^\s*#" "$tmp" | grep -qE "(proxy_pass|fastcgi_pass|uwsgi_pass|include\s+/|lua_|perl_|upstream)" ; then
|
|
||||||
log "[$user] RECHAZADO: directivas prohibidas detectadas ($conffile)"
|
|
||||||
echo "ERROR: Directivas proxy/include/lua no permitidas" > "$error_file"
|
|
||||||
chown $user:www-data "$error_file" 2>/dev/null
|
|
||||||
notify_xmpp "❌ [$user] Config RECHAZADA: directivas prohibidas ($conffile)"
|
|
||||||
rm -f "$tmp"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Hacer backup del config actual si existe
|
|
||||||
[[ -f "$dest" ]] && cp "$dest" "$backup"
|
|
||||||
|
|
||||||
# Copiar nuevo config a sites-available
|
|
||||||
cp "$tmp" "$dest"
|
|
||||||
|
|
||||||
# Asegurar que el symlink existe
|
|
||||||
ln -sf "$dest" "$enabled"
|
|
||||||
|
|
||||||
# Validar sintaxis nginx con la nueva config
|
|
||||||
local nginx_output
|
|
||||||
nginx_output=$(nginx -t 2>&1)
|
|
||||||
|
|
||||||
if echo "$nginx_output" | grep -q "successful"; then
|
|
||||||
# Sintaxis OK - recargar nginx
|
|
||||||
systemctl reload nginx
|
|
||||||
log "[$user] OK: Config desplegada ($conffile → $site_id) y nginx recargado"
|
|
||||||
|
|
||||||
# Analizar CSP y cabeceras de seguridad
|
|
||||||
local csp_analysis
|
|
||||||
csp_analysis=$(analyze_csp "$dest")
|
|
||||||
|
|
||||||
# Analizar redirección HTTP → HTTPS
|
|
||||||
local https_check
|
|
||||||
https_check=$(check_https_redirect "$dest" "$expected_name")
|
|
||||||
|
|
||||||
# Mensaje de deploy según sea sitio principal o sub-sitio
|
|
||||||
local deploy_msg
|
|
||||||
if [[ -n "$app_suffix" ]]; then
|
|
||||||
deploy_msg="✅ [$user] Config desplegada OK ($conffile → ${expected_name}.qu3v3d0.tech)"
|
|
||||||
else
|
|
||||||
deploy_msg="✅ [$user] Config desplegada OK ($conffile)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# XMPP al profesor (resumen + todos los análisis)
|
|
||||||
notify_xmpp "$deploy_msg
|
|
||||||
$https_check$csp_analysis${naming_check:+
|
|
||||||
$naming_check}"
|
|
||||||
|
|
||||||
# nginx-status.log (resumen para el alumno)
|
|
||||||
{
|
|
||||||
echo "OK: Config desplegada desde $conffile $(date)"
|
|
||||||
if [[ -n "$app_suffix" ]]; then
|
|
||||||
echo "🌐 Sitio: https://${expected_name}.qu3v3d0.tech"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
echo "$https_check"
|
|
||||||
echo "$csp_analysis"
|
|
||||||
if [[ -n "$naming_check" ]]; then
|
|
||||||
echo "$naming_check"
|
|
||||||
fi
|
|
||||||
} > "$status_file"
|
|
||||||
chown $user:www-data "$status_file" 2>/dev/null
|
|
||||||
|
|
||||||
# practica-status.log (detalle completo)
|
|
||||||
{
|
|
||||||
echo "=== Análisis de $conffile — $(date) ==="
|
|
||||||
if [[ -n "$app_suffix" ]]; then
|
|
||||||
echo "🌐 Sitio: https://${expected_name}.qu3v3d0.tech"
|
|
||||||
echo "📁 Root: $expected_root"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
echo "--- Redirección HTTPS ---"
|
|
||||||
echo "$https_check"
|
|
||||||
echo "--- Seguridad (CSP y cabeceras) ---"
|
|
||||||
echo "$csp_analysis"
|
|
||||||
if [[ -n "$naming_check" ]]; then
|
|
||||||
echo "--- Nomenclatura ---"
|
|
||||||
echo "$naming_check"
|
|
||||||
fi
|
|
||||||
echo "---"
|
|
||||||
echo "Revisa el enunciado de tu práctica para los entregables pendientes."
|
|
||||||
} > "$practica_file"
|
|
||||||
chown $user:www-data "$practica_file" 2>/dev/null
|
|
||||||
|
|
||||||
rm -f "$error_file"
|
|
||||||
rm -f "$backup"
|
|
||||||
else
|
|
||||||
# Error de sintaxis - revertir
|
|
||||||
log "[$user] RECHAZADO: Error de sintaxis nginx ($conffile → $site_id)"
|
|
||||||
|
|
||||||
# Humanizar el error
|
|
||||||
local pistas
|
|
||||||
pistas=$(humanize_nginx_error "$nginx_output")
|
|
||||||
|
|
||||||
# nginx-error.log con error original + pistas
|
|
||||||
{
|
|
||||||
echo "$nginx_output" | tail -5
|
|
||||||
echo "ERROR: Sintaxis nginx inválida."
|
|
||||||
if [[ -n "$pistas" ]]; then
|
|
||||||
echo ""
|
|
||||||
echo "--- Pistas ---"
|
|
||||||
echo "$pistas"
|
|
||||||
fi
|
|
||||||
} > "$error_file"
|
|
||||||
chown $user:www-data "$error_file" 2>/dev/null
|
|
||||||
|
|
||||||
# XMPP al profesor con pistas
|
|
||||||
if [[ -n "$pistas" ]]; then
|
|
||||||
notify_xmpp "❌ [$user] Config RECHAZADA: sintaxis inválida ($conffile)
|
|
||||||
$pistas"
|
|
||||||
else
|
|
||||||
notify_xmpp "❌ [$user] Config RECHAZADA: sintaxis inválida ($conffile)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restaurar backup si existe
|
|
||||||
if [[ -f "$backup" ]]; then
|
|
||||||
cp "$backup" "$dest"
|
|
||||||
log "[$user] Config anterior de $site_id restaurada"
|
|
||||||
else
|
|
||||||
rm -f "$dest" "$enabled"
|
|
||||||
log "[$user] Config $site_id eliminada (no había backup)"
|
|
||||||
fi
|
|
||||||
rm -f "$backup"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$tmp"
|
|
||||||
}
|
|
||||||
|
|
||||||
log "=== Nginx User Config Watcher v3 iniciado (*.conf, multi-sitio) ==="
|
|
||||||
notify_xmpp "🚀 [zzz] Nginx Config Watcher v3 iniciado (*.conf, multi-sitio)"
|
|
||||||
|
|
||||||
# Bucle principal con inotifywait - detecta cualquier *.conf
|
|
||||||
# v3.1: también detecta delete/moved_from para limpiar sites huérfanos
|
|
||||||
inotifywait -m -r -e close_write,moved_to,delete,moved_from --format "%e %w%f" "$WATCH_DIR" 2>/dev/null | while read event filepath; do
|
|
||||||
# Captura: /home/USER/html/ARCHIVO.conf
|
|
||||||
if [[ "$filepath" =~ ^/home/([^/]+)/html/([^/]+.conf)$ ]]; then
|
|
||||||
user="${BASH_REMATCH[1]}"
|
|
||||||
conffile="${BASH_REMATCH[2]}"
|
|
||||||
if [[ "$event" == *DELETE* || "$event" == *MOVED_FROM* ]]; then
|
|
||||||
log "[$user] Detectado borrado de $conffile"
|
|
||||||
undeploy_site "$user" "$conffile"
|
|
||||||
else
|
|
||||||
log "[$user] Detectado cambio en $conffile"
|
|
||||||
sleep 1 # Esperar a que el archivo esté completo
|
|
||||||
validate_and_deploy "$user" "$conffile"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Watch ASIR1 Python uploads via SFTP and notify XMPP
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
ExecStart=/usr/local/bin/python-upload-watcher.sh
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# /usr/local/bin/python-upload-watcher.sh
|
|
||||||
# Watcher para entregas de Programación ASIR1 via SFTP
|
|
||||||
# v7 — Nombre completo en notificaciones ([María Jara] en vez de [jara])
|
|
||||||
#
|
|
||||||
# - Batching por carpeta: UN mensaje XMPP con listado completo
|
|
||||||
# - Re-entregas: detecta delete+recreate (<120s)
|
|
||||||
# - Windows-safe: ignora Thumbs.db, desktop.ini, .DS_Store, *.tmp, __pycache__, *.pyc
|
|
||||||
|
|
||||||
WATCH_DIR="/home"
|
|
||||||
|
|
||||||
# --- Mapa usuario → nombre completo ---
|
|
||||||
fullname() {
|
|
||||||
case "$1" in
|
|
||||||
barja) echo "Alex Barja" ;;
|
|
||||||
barrios) echo "Andrés Barrios" ;;
|
|
||||||
cayo) echo "Jared Cayo" ;;
|
|
||||||
contrera) echo "Luciano Contrera" ;;
|
|
||||||
duque) echo "Jorge Duque" ;;
|
|
||||||
florea) echo "Alejandro Florea" ;;
|
|
||||||
gomes) echo "Gabriel Gomes" ;;
|
|
||||||
izquierdo) echo "Daniel Izquierdo" ;;
|
|
||||||
jara) echo "María Jara" ;;
|
|
||||||
lillo) echo "Fernando Lillo" ;;
|
|
||||||
linares) echo "Christopher Linares" ;;
|
|
||||||
macedo) echo "Eduardo Macedo" ;;
|
|
||||||
martinez) echo "Daniel Martínez" ;;
|
|
||||||
munoz) echo "Jordy Muñoz" ;;
|
|
||||||
olcina) echo "Jorge Olcina" ;;
|
|
||||||
ponce) echo "Francisco Ponce" ;;
|
|
||||||
posada) echo "Santiago Posada" ;;
|
|
||||||
quiroz) echo "Alexander Quiroz" ;;
|
|
||||||
reynoso) echo "Jorge Reynoso" ;;
|
|
||||||
sierra) echo "Leonel Sierra" ;;
|
|
||||||
torrero) echo "Mario Torrero" ;;
|
|
||||||
*) echo "$1" ;; # fallback: username tal cual
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
LOG="/var/log/python-upload-watcher.log"
|
|
||||||
BATCH_DIR="/tmp/python-watcher-batches"
|
|
||||||
DELETE_DIR="/tmp/python-watcher-deletes"
|
|
||||||
QUIET_SECONDS=10
|
|
||||||
|
|
||||||
mkdir -p "$BATCH_DIR" "$DELETE_DIR"
|
|
||||||
|
|
||||||
log() {
|
|
||||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG"
|
|
||||||
}
|
|
||||||
|
|
||||||
notify_xmpp() {
|
|
||||||
/usr/local/bin/xmpp-notify.py "$1" >/dev/null 2>&1 &
|
|
||||||
}
|
|
||||||
|
|
||||||
is_junk() {
|
|
||||||
case "${1,,}" in
|
|
||||||
thumbs.db|desktop.ini|.ds_store|*.tmp|~\$*|~*.tmp) return 0 ;;
|
|
||||||
__pycache__|*.pyc|*.pyo) return 0 ;;
|
|
||||||
.git|.venv|.env|node_modules|.idea|.vscode) return 0 ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# Filtrar paths que contengan directorios basura
|
|
||||||
is_junk_path() {
|
|
||||||
case "$1" in
|
|
||||||
*/__pycache__/*|*/.git/*|*/.venv/*|*/node_modules/*|*/.idea/*|*/.vscode/*) return 0 ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
file_icon() {
|
|
||||||
case "${1,,}" in
|
|
||||||
*.py) echo "🐍" ;;
|
|
||||||
*.zip|*.rar|*.tar*|*.gz|*.bz2|*.7z) echo "📦" ;;
|
|
||||||
*.md) echo "📝" ;;
|
|
||||||
*.txt) echo "📄" ;;
|
|
||||||
*.pdf) echo "📕" ;;
|
|
||||||
*.docx|*.doc) echo "📘" ;;
|
|
||||||
*.png|*.jpg|*.jpeg|*.gif) echo "🖼️" ;;
|
|
||||||
*) echo "📎" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
batch_key() {
|
|
||||||
echo "${1}_${2}" | tr '/ ' '__'
|
|
||||||
}
|
|
||||||
|
|
||||||
flush_batch() {
|
|
||||||
local batchfile="$1"
|
|
||||||
[[ ! -f "$batchfile" ]] && return
|
|
||||||
|
|
||||||
local user dir tag file_count file_list label msg
|
|
||||||
user=$(sed -n '1p' "$batchfile")
|
|
||||||
dir=$(sed -n '2p' "$batchfile")
|
|
||||||
tag=$(sed -n '3p' "$batchfile")
|
|
||||||
file_count=$(tail -n +4 "$batchfile" | wc -l)
|
|
||||||
|
|
||||||
[[ "$file_count" -eq 0 ]] && { rm -f "$batchfile"; return; }
|
|
||||||
|
|
||||||
file_list=$(tail -n +4 "$batchfile")
|
|
||||||
label="Entrega"
|
|
||||||
[[ "$tag" == "re-entrega" ]] && label="♻️ Re-entrega"
|
|
||||||
|
|
||||||
local display_name
|
|
||||||
display_name=$(fullname "$user")
|
|
||||||
msg="📁 [$display_name] $label ASIR1: $dir/ ($file_count ficheros)
|
|
||||||
$file_list"
|
|
||||||
|
|
||||||
log "[$user] BATCH FLUSH ($tag): $dir/ → $file_count ficheros"
|
|
||||||
notify_xmpp "$msg"
|
|
||||||
rm -f "$batchfile"
|
|
||||||
}
|
|
||||||
|
|
||||||
flush_daemon() {
|
|
||||||
while true; do
|
|
||||||
sleep 3
|
|
||||||
for batchfile in "$BATCH_DIR"/*.batch; do
|
|
||||||
[[ ! -f "$batchfile" ]] && continue
|
|
||||||
local last_mod now age
|
|
||||||
last_mod=$(stat -c%Y "$batchfile" 2>/dev/null) || continue
|
|
||||||
now=$(date +%s)
|
|
||||||
age=$(( now - last_mod ))
|
|
||||||
(( age >= QUIET_SECONDS )) && flush_batch "$batchfile"
|
|
||||||
done
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
flush_daemon &
|
|
||||||
FLUSH_PID=$!
|
|
||||||
trap "kill $FLUSH_PID 2>/dev/null" EXIT
|
|
||||||
|
|
||||||
log "=== Python Upload Watcher v7 iniciado ==="
|
|
||||||
notify_xmpp "🚀 [zzz] Python Upload Watcher v7 iniciado (ASIR1)"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# Bucle principal — sin 'local', todo variables globales
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
inotifywait -m -r \
|
|
||||||
-e close_write -e moved_to -e create -e delete -e moved_from \
|
|
||||||
--format "%e %w%f" "$WATCH_DIR" 2>/dev/null | \
|
|
||||||
while IFS= read -r line; do
|
|
||||||
|
|
||||||
_event="${line%% /*}"
|
|
||||||
_filepath="/${line#* /}"
|
|
||||||
|
|
||||||
[[ ! "$_filepath" =~ ^/home/([^/]+)/python/(.+)$ ]] && continue
|
|
||||||
|
|
||||||
_user="${BASH_REMATCH[1]}"
|
|
||||||
_relpath="${BASH_REMATCH[2]}"
|
|
||||||
_filename=$(basename "$_relpath")
|
|
||||||
|
|
||||||
is_junk "$_filename" && continue
|
|
||||||
is_junk_path "$_relpath" && continue
|
|
||||||
|
|
||||||
# --- DELETE / MOVED_FROM ---
|
|
||||||
if [[ "$_event" == *DELETE* || "$_event" == *MOVED_FROM* ]]; then
|
|
||||||
if [[ "$_relpath" != */* ]]; then
|
|
||||||
_bk=$(batch_key "$_user" "$_relpath")
|
|
||||||
echo "$(date +%s)" > "$DELETE_DIR/$_bk"
|
|
||||||
rm -f "$BATCH_DIR/${_bk}.batch"
|
|
||||||
log "[$_user] BORRADA: $_relpath/"
|
|
||||||
fi
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- CREATE directorio ---
|
|
||||||
if [[ "$_event" == *CREATE* && -d "$_filepath" ]]; then
|
|
||||||
# Solo carpetas de primer nivel bajo python/
|
|
||||||
if [[ "$_relpath" != */* ]]; then
|
|
||||||
_bk=$(batch_key "$_user" "$_relpath")
|
|
||||||
_tag="new"
|
|
||||||
|
|
||||||
if [[ -f "$DELETE_DIR/$_bk" ]]; then
|
|
||||||
_del_ts=$(cat "$DELETE_DIR/$_bk")
|
|
||||||
_now=$(date +%s)
|
|
||||||
(( _now - _del_ts < 120 )) && _tag="re-entrega"
|
|
||||||
rm -f "$DELETE_DIR/$_bk"
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n%s\n%s\n' "$(fullname "$_user")" "$_relpath" "$_tag" > "$BATCH_DIR/${_bk}.batch"
|
|
||||||
log "[$_user] CARPETA ($_tag): $_relpath/ — batch abierto en $BATCH_DIR/${_bk}.batch"
|
|
||||||
fi
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- CREATE fichero → skip (close_write viene después) ---
|
|
||||||
[[ "$_event" == *CREATE* ]] && continue
|
|
||||||
|
|
||||||
# --- close_write / moved_to → fichero completado ---
|
|
||||||
_size=$(stat -c%s "$_filepath" 2>/dev/null || echo "?")
|
|
||||||
_icon=$(file_icon "$_filename")
|
|
||||||
|
|
||||||
if [[ "$_relpath" == */* ]]; then
|
|
||||||
_topdir="${_relpath%%/*}"
|
|
||||||
_bk=$(batch_key "$_user" "$_topdir")
|
|
||||||
_batchfile="$BATCH_DIR/${_bk}.batch"
|
|
||||||
|
|
||||||
if [[ -f "$_batchfile" ]]; then
|
|
||||||
_subpath="${_relpath#*/}"
|
|
||||||
echo " $_icon $_subpath ($_size bytes)" >> "$_batchfile"
|
|
||||||
log "[$_user] +BATCH: $_relpath ($_size bytes)"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sin batch → individual
|
|
||||||
log "[$_user] Subido: $_relpath ($_size bytes)"
|
|
||||||
notify_xmpp "$_icon [$(fullname "$_user")] Entrega ASIR1: $_relpath ($_size bytes)"
|
|
||||||
else
|
|
||||||
log "[$_user] Subido: $_relpath ($_size bytes)"
|
|
||||||
notify_xmpp "$_icon [$(fullname "$_user")] Entrega ASIR1: $_relpath ($_size bytes)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
done
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Notificador XMPP one-shot para zzz.qu3v3d0.tech
|
|
||||||
Uso: xmpp-notify.py "mensaje"
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
import slixmpp
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.WARNING)
|
|
||||||
|
|
||||||
class NotifyBot(slixmpp.ClientXMPP):
|
|
||||||
def __init__(self, msg):
|
|
||||||
super().__init__("zzz@librebits.info", "zzz2025")
|
|
||||||
self.msg = msg
|
|
||||||
self.add_event_handler("session_start", self.start)
|
|
||||||
|
|
||||||
async def start(self, event):
|
|
||||||
self.send_message(mto="jla@librebits.info", mbody=self.msg, mtype="chat")
|
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
self.disconnect()
|
|
||||||
|
|
||||||
async def main(msg):
|
|
||||||
bot = NotifyBot(msg)
|
|
||||||
bot.connect()
|
|
||||||
await asyncio.sleep(8)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Uso: xmpp-notify.py \"mensaje\"")
|
|
||||||
sys.exit(1)
|
|
||||||
asyncio.run(main(sys.argv[1]))
|
|
||||||
Loading…
Reference in New Issue