# Web: motor HTTP, interfaz y panel de administración

> Diseño previsto; aún no implementado.

## Por qué un motor HTTP propio

- **Sin administrador.** La 2.0 usaba `HttpListener`, que en Windows va sobre http.sys:
  escuchar fuera de `localhost` exige ser administrador o reservar la URL con `netsh` (que
  también lo exige). Cambiar la ruta (un "directorio virtual") no lo evita. Un `Socket` propio
  no necesita permisos en Windows con ningún puerto; en Linux y macOS los puertos por debajo de
  1024 siguen requiriendo privilegios, por eso los puertos por defecto son altos.
- **Tolerancia con clientes antiguos.** Los navegadores de los 90 envían peticiones que los
  servidores modernos rechazan (saltos de línea solo `LF`, HTTP/1.0 sin `Host`, cabeceras poco
  normales). Con un motor propio se aceptan.
- **Native AOT.** Sin ASP.NET Core, `sunfish-cli` puede ser un binario nativo pequeño.

## Alcance del motor

- **Versiones:** HTTP/0.9 (`GET /ruta` sin versión: solo el cuerpo), HTTP/1.0 y HTTP/1.1.
- **Métodos:** `GET`, `HEAD`, `POST` (formularios y subidas), `PUT` (subidas modernas) y los de
  WebDAV ([FTP-Y-WEBDAV.md](FTP-Y-WEBDAV.md)).
- **Analizador tolerante:** acepta `CRLF` o `LF`; nombres de cabecera sin distinguir mayúsculas;
  `Host` opcional en 1.0.
- **Conexiones:** keep-alive en 1.1 y en 1.0 si se pide `Connection: keep-alive`; si no, se
  cierra al terminar.
- **Respuestas:**
  - Los ficheros llevan siempre `Content-Length`: los clientes antiguos no conocen `chunked`, y
    así muestran el progreso.
  - Las páginas generadas se envían con `chunked` a clientes 1.1 y cerrando la conexión a los 1.0.
- **Rangos** (`Range: bytes=`, uno solo) para reanudar descargas; `If-Modified-Since` → 304.
- **Cuerpos:** `Content-Length` o `chunked`; `Expect: 100-continue`.
- **Autenticación:** HTTP Basic (la soportan hasta Mosaic y los primeros Netscape).
- **Límites:**
  - Tamaño de la línea de petición y de las cabeceras (8 KB).
  - Plazos de inactividad (contra conexiones lentas a propósito).
  - Conexiones por IP.
  - Tamaño máximo de subida, opcional.
- **Puerto por defecto:** 8080, configurable; IPv4 e IPv6.
- **Sin TLS** (fuera de alcance; para eso existe Arrejollal).

## Interfaz web

### Dos modos con la misma página

| Modo | Para quién | Qué cambia |
|---|---|---|
| **Clásico** | Navegadores antiguos (por defecto si el User-Agent no es moderno) | HTML 3.2/4.01 Transitional, maquetado con tablas, sin JavaScript, ISO-8859-1 |
| **Moderno** | Navegadores actuales | La misma estructura, UTF-8, un CSS sencillo y un JS pequeño para arrastrar y soltar y para subir por trozos con progreso |

- El servidor decide por el User-Agent, dentro de los **perfiles de cliente** (`dos`, `win16`,
  `mac`, `text`, `classic`, `modern`) descritos en [CLIENTES.md](CLIENTES.md), que además ajustan
  nombres (8.3, 31 caracteres), juego de caracteres y entrega de ficheros de Mac.
- **Forzar el perfil:** `?p=…`. El perfil forzado se propaga en los enlaces de la página
  (algunos clientes antiguos no guardan cookies) y se recuerda en una cookie cuando se puede.
- **Fuente sin serifa** en los dos modos (CSS en el moderno, `<font face>` en el clásico).
- El modo clásico **no incluye ningún `<script>`**: algunos navegadores antiguos muestran un
  diálogo de error por cada script que no entienden.
- Todo funciona sin JS: el JS del modo moderno solo mejora lo que ya hacen los formularios.
- Sin recursos externos (la 2.0 cargaba fuentes e iconos de Internet; en una LAN sin salida se
  veía rota). Iconos en **GIF** incrustados: IE3, Netscape 1–3 y Mosaic no muestran PNG.

### Juego de caracteres

- **Modo clásico:** la página se declara ISO-8859-1.
  - Los caracteres Latin-1 se escriben como entidades HTML (`&aacute;`), que entienden incluso
    los navegadores de HTML 2.0.
  - Los demás caracteres se muestran como `?` (o transliterados cuando haya equivalente).
- **Modo moderno:** UTF-8.
- **Los enlaces llevan siempre los bytes UTF-8 reales en `%XX`**, así que funcionan aunque el
  texto se vea transliterado: el navegador devuelve los mismos bytes al pedir.
- **Lo que envía un navegador antiguo** (nombres de carpeta nueva, nombres en subidas) llega en
  el juego de caracteres de la página o en el del sistema.
  - El servidor intenta UTF-8 y, si no es válido, usa el juego configurado para clientes
    antiguos (ISO-8859-1 por defecto; MacRoman para Mac clásico).
  - **Pendiente** de probar con clientes reales.

### Navegador de directorios

- **Tabla** con icono, nombre (enlace), tamaño, fecha y acciones. Se ordena por nombre, tamaño o
  fecha con un enlace en la cabecera (`?sort=`), sin JS.
- **Migas de pan** con la ruta y enlace a la raíz.
- **Formularios**, según los permisos del share:
  - subir fichero (`multipart/form-data`, RFC 1867, desde Netscape 2 e IE3/IE4);
  - crear carpeta;
  - descargar en ZIP.
- **Renombrar y borrar** llevan a una página de confirmación con un formulario POST. Nunca hay
  cambios por GET (la 2.0 borraba con `?action=delete`).
- **Mac clásico:** los ficheros con *resource fork* se entregan en MacBinary a los Mac clásicos
  de forma automática; para los demás clientes, enlaces `[.bin]` y `[.hqx]` (ver
  [MAC-CLASICO.md](MAC-CLASICO.md)).
- **DOS y Windows 3.1:** alias 8.3 en el listado y en las descargas (ver [CLIENTES.md](CLIENTES.md)).
- **`?$list`** muestra este navegador aunque la carpeta tenga `index.html` (si el share tiene
  `Listing`). En el menú raíz y en los listados, las carpetas con índice llevan un pequeño enlace
  `[contenido]` que apunta a él.
- **Escapado:** todo nombre que se escribe en el HTML se escapa (la 2.0 no lo hacía).

### Subidas

- **Multipart:** el analizador trabaja **en flujo** y escribe directamente a disco; los límites
  pueden quedar partidos entre búferes. La 2.0 leía byte a byte y lo guardaba todo en memoria.
- **Nombre del fichero:** se queda solo el nombre. IE antiguo envía la ruta completa
  (`C:\WINDOWS\fichero.txt`) y Mac clásico puede usar `:` como separador.
- **Escritura segura:** se escribe a un temporal en la misma carpeta y se renombra al terminar.
  Una subida cortada no deja un fichero a medias ni pisa el existente.
- **Modo moderno:** `sunfish.js` envía cada fichero con `PUT` a su URL en trozos de 1 MiB
  (`Content-Range: bytes inicio-fin/total` y `X-Sunfish-Upload: <id>`). Los trozos deben llegar en
  orden; el temporal es `.sunfish-<id>.part` y el último trozo lo renombra a su sitio (202 por
  trozo, 201/204 al terminar). Un `PUT` sin `Content-Range` sube el fichero entero.
- Respeta `Upload` y `Overwrite`; comprueba el espacio libre si el tamaño se conoce de antemano.
- **Pendiente:** limpiar `.sunfish-*.part` abandonados por subidas que no terminaron.

### Formularios y parámetros

| Petición | Qué hace |
|---|---|
| `POST carpeta/` multipart, `a=upload` | Subir ficheros (campo `f`, varios a la vez) |
| `POST carpeta/` formulario, `a=mkdir&name=` | Crear carpeta |
| `POST carpeta/` formulario, `a=rename&name=&to=` | Renombrar dentro de la carpeta |
| `POST carpeta/` formulario, `a=delete&name=` | Borrar fichero o carpeta entera |
| `GET carpeta/?$rename=nombre` | Página para escribir el nombre nuevo |
| `GET carpeta/?$delete=nombre` | Página de confirmación del borrado |
| `GET carpeta/?$zip` | Carpeta en ZIP |

- Todo `POST` lleva `t`, el *token* anti-CSRF de esa acción y carpeta (ver
  [SEGURIDAD.md](SEGURIDAD.md)). En multipart, `a` y `t` van antes que los ficheros y se validan
  antes de escribir nada.
- Tras un `POST` correcto se vuelve a la carpeta con 303 (HTTP/1.1) o 302 (HTTP/1.0, que no
  conoce el 303).
- Los nombres nuevos no pueden empezar por `.` (quedarían ocultos en el listado).

### Usuarios

Un share con `Users` exige HTTP Basic. Las cuentas se gestionan con
`sunfish-cli user add|passwd|del|list` (contraseña pedida sin eco). Ver
[SEGURIDAD.md](SEGURIDAD.md#contraseñas).

### Descargar carpetas en ZIP

- Se genera **en flujo**, sin fichero temporal (la 2.0 creaba un ZIP temporal completo).
- **Compatible con descompresores antiguos** (PKZIP 2.x, StuffIt):
  - Método **Stored** con el CRC calculado antes de cada fichero. No necesita
    *data descriptors* y permite enviar `Content-Length`, a costa de leer dos veces cada fichero.
  - Nombres no ASCII: en CP437 para clientes antiguos (si se pueden representar) y en UTF-8 con
    su indicador para los modernos, que leen como UTF-8 los nombres sin indicador.
  - Sin ocultos ni enlaces; las locations hijas (otros shares) no se incluyen.
  - **Pendiente:** ZIP64 (hoy, más de 4 GiB o 65535 entradas da un error) y Deflate opcional.

### Autodescarga del servidor

`/$sunfish/download` entrega el propio ejecutable que está sirviendo (como hacía la 2.0), para
repartirlo por la LAN. Se desactiva con `SelfDownload = false`.

## Panel de administración

En `/$sunfish/admin/`, **desactivado por defecto**.

Implementado en `Web/WebAdmin.cs` (fase 7). Se activa desde la GUI (ventana de información),
con `sunfish-cli admin enable` o con `[Admin] Enabled = true` y una contraseña.

- **Acceso:** usuario `admin` con contraseña (HTTP Basic); hash PBKDF2 en `[Admin]`. Mismo
  bloqueo por intentos fallidos que el resto. Desactivado o sin contraseña responde 404.
- **`AllowFrom`:** IP sueltas, redes CIDR (`192.168.1.0/24`, `fd00::/8`) o `*`. Por defecto,
  `127.0.0.1, ::1`. Desde otra dirección responde 403. Las IPv4 que llegan como IPv6
  (`::ffff:a.b.c.d`) se comparan como IPv4.
- **Páginas:**
  - `/$sunfish/admin/`: estado (HTTP y FTP), direcciones con su enlace **[QR]**, servidor
    (nombre, menú raíz, FTP sí/no y puerto), shares (editar, quitar), usuarios (alta,
    contraseña, baja) y ajustes del propio panel;
  - `/$sunfish/admin/share` y `share?n=NOMBRE`: alta y edición de un share con todas sus
    opciones (las mismas validaciones que la GUI);
  - `/$sunfish/admin/log`: el registro reciente;
  - `/$sunfish/admin/qr?u=URL`: PNG con el QR (solo `http://` y `ftp://`, hasta 300 caracteres).
- **El HTTP en sí no se cambia desde el panel** (puerto, interfaces, activarlo): reiniciar el
  servidor HTTP cortaría la propia conexión del panel. Se cambia desde la aplicación.
- **HTML clásico** también, para poder administrar desde cualquier navegador; sin JavaScript.
- **Formularios POST con *token* anti-CSRF** (ver [SEGURIDAD.md](SEGURIDAD.md)). Tras guardar,
  redirección 303 (302 en HTTP/1.0) a `?ok`; si hay un error, la misma página con el aviso.
- Los cambios se guardan en el fichero de configuración al momento (o solo se aplican en el
  modo rápido, que no tiene fichero).
- Borrar un usuario lo quita también de los shares; `admin` no vale como nombre de usuario.
- Aviso visible: la contraseña viaja en claro por la red.
- **Pendiente:** lista de conexiones activas.
