# FTP y WebDAV

> Ambos sirven **el mismo árbol de shares** que la web, con las mismas *locations*, permisos y
> usuarios ([SHARES-Y-RUTAS.md](SHARES-Y-RUTAS.md)). **FTP: hecho (fase 4). WebDAV, cliente
> WebDAV y DNS-SD: hechos (fase 5).**

## FTP

Para los clientes con los que la web se queda corta: en Mac clásico los clientes FTP gráficos
eran la forma habitual de mover ficheros, en DOS y Windows 3.1 hay clientes FTP para las pilas
TCP/IP de la época, y Windows 95 trae `ftp.exe`.

**Implementación** (`src/Sunfish.Core/Ftp/`): `FtpServer` (escucha, límite por IP),
`FtpSession` (un hilo de control por conexión; atiende `ABOR` mientras transfiere), `DataChannel`
(pasivo/activo), `FtpPath` y `FtpListing`. Detalles que se han decidido al implementar:

- **Anónimo siempre permitido** (`anonymous` o `ftp`, cualquier contraseña), pero solo ve los
  shares sin `Users`. Un share con `Ftp = false` no aparece en FTP.
- **Opciones en `[Server]`:** `Ftp` (interruptor), `FtpPort` (2121), `FtpPassivePorts`
  (`50000-50050`; vacío o inválido = cualquier puerto libre), `FtpPublicAddress` (IP anunciada
  en `PASV` tras NAT) y `FtpCharset` (`iso-8859-1` por defecto; también `macroman`, `cp437`,
  `cp850`, `windows-1252`, `utf-8`).
- **Juego de caracteres:** lo que llega se lee como UTF-8 si es válido y, si no, con el juego de
  la sesión. `LIST`/`NLST` y las respuestas usan UTF-8 tras `OPTS UTF8 ON` y si no, el juego de la
  sesión; `MLSD` siempre UTF-8 (RFC 3659).
- **`STOR` normal** pasa por un temporal y renombrado (como la web); **`APPE` y `REST`+`STOR`**
  escriben sobre el fichero existente para reanudar, y por eso exigen `Overwrite`.
- **`RMD`** solo borra carpetas vacías (el borrado recursivo queda para la web). La raíz de un
  share y las carpetas virtuales no se pueden borrar, renombrar ni usar para crear.
- **`TYPE A`** convierte LF en CRLF al enviar; al recibir se guarda tal cual.
- Probado con `curl` (pasivo, `EPSV`) y con `ftp.exe` de Windows (activo, `PORT`).

### Conexión

- **Puerto de control:** 2121 por defecto (el 21 requiere privilegios en Linux/macOS),
  configurable.
- **Modo pasivo** (`PASV`, `EPSV`): rango de puertos configurable (`FtpPassivePorts`). Se anuncia
  la IP local de la conexión de control; `FtpPublicAddress` opcional para equipos tras NAT.
- **Modo activo** (`PORT`, `EPRT`): lo usan por defecto clientes antiguos como `ftp.exe`. El
  servidor solo conecta a la **misma IP** que abrió el control (contra el "FTP bounce") y nunca a
  puertos por debajo de 1024. El puerto de origen no es el 20 (no requiere privilegios); los
  clientes lo aceptan.
- **IPv4 e IPv6** (`EPSV`/`EPRT`).
- **Sin cifrado** (ni FTPS): en claro, como la web.

### Autenticación

- **Usuario `anonymous`** (o `ftp`), con cualquier contraseña: ve los shares públicos.
- **Usuario y contraseña de `[User …]`:** ve además los shares cuyo `Users` lo incluye.
- **Contraseñas fallidas:** retardo creciente y límite de intentos por IP.

### Comandos

| Grupo | Comandos |
|---|---|
| Sesión | `USER` `PASS` `QUIT` `NOOP` `SYST` (`UNIX Type: L8`) `FEAT` `OPTS UTF8` `HELP` `STAT` |
| Navegación | `PWD` `CWD` `CDUP` |
| Listados | `LIST` (formato `ls -l`, el que entienden los clientes antiguos) `NLST` `MLSD` `MLST` |
| Transferencia | `TYPE` (`A`/`I`) `MODE S` `STRU F` `RETR` `STOR` `APPE` `REST` (reanudar) `ABOR` |
| Información | `SIZE` `MDTM` |
| Cambios | `MKD` `RMD` `DELE` `RNFR`/`RNTO` |

- `TYPE A` convierte los saltos de línea a `CRLF` al enviar; `TYPE I` (binario) es el habitual.
- Los permisos del share se aplican igual que en la web: `STOR` sobre un fichero existente
  necesita `Overwrite`, `DELE` necesita `Delete`, etc. Las subidas también van a un temporal y se
  renombran al terminar.
- Los directorios virtuales de las *locations* aparecen en `LIST` como carpetas.

### Juego de caracteres de los nombres

- Si el cliente activa `OPTS UTF8 ON` (o usa `MLSD`), UTF-8.
- Si la sesión tiene perfil (ver abajo): `dos` → CP437 (o CP850), `win16` → Windows-1252,
  `mac` → MacRoman.
- Si no, el juego configurado en `FtpCharset`: `iso-8859-1` (por defecto), `macroman`, `cp437`,
  `cp850` o `windows-1252`.

### Nombres cortos y Mac en FTP

FTP no dice qué cliente se conecta, así que los perfiles de [CLIENTES.md](CLIENTES.md) se eligen
por sesión:

- **Sufijo en el usuario:** `anonymous+dos`, `ana+dos`, `anonymous+mac`… (`+dos` y `+win16`
  activan alias 8.3; `+mac` activa la entrega en MacBinary). Funciona con cualquier cliente,
  porque basta con escribirlo al conectar.
- **Comando `SITE`:** `SITE PROFILE dos|win16|mac|none`, para clientes que permiten enviar
  comandos.
- **Por defecto (sin perfil):** nombres reales y ficheros tal cual.
- **Subidas:** un `STOR` con MacBinary II/III o BinHex válidos se decodifica siempre, como en la
  web (salvo `Mac = false`). Ver [MAC-CLASICO.md](MAC-CLASICO.md).
- **Perfil `mac`:** los ficheros con `._` se listan como `nombre.bin` y se descargan en MacBinary.
  Los clientes FTP de Mac clásico suelen decodificarlo solos. Los nombres de más de 31 caracteres
  no se acortan en FTP (sí en las descargas web).

## WebDAV

Sobre el mismo puerto HTTP, activo por defecto en todos los shares (`WebDav = false` lo
desactiva), para montar los shares como una carpeta desde sistemas modernos (y algunos
antiguos). Es también el protocolo con el que un Sunfish explora a otro (ver más abajo).

### Alcance

- **Clase 1:** `OPTIONS`, `PROPFIND` (profundidad 0 y 1; `infinity` se rechaza), `PROPPATCH`
  (se acepta y se ignora lo que no se aplica), `MKCOL`, `GET`, `PUT`, `DELETE`, `COPY` y `MOVE`
  dentro del mismo share.
- **`LOCK`/`UNLOCK` simulados** (se anuncia clase 2): Finder de macOS y el redirector de Windows
  los exigen para escribir. Se devuelven *tokens* válidos, pero no impiden accesos de otros
  clientes.
- **Propiedades:** `displayname`, `resourcetype`, `getcontentlength`, `getcontenttype`,
  `getlastmodified`, `creationdate`, `getetag`.
- **Cabeceras:** `DAV: 1, 2` y `MS-Author-Via: DAV` (la piden las "Carpetas web" de Windows
  98/2000).
- **XML:** se lee con `XmlReader` con `DtdProcessing.Prohibit` (sin entidades externas).
- **Permisos:** los mismos del share; `Listing` controla `PROPFIND` de profundidad 1.

**Implementación** (`src/Sunfish.Core/Web/WebDav.cs`, dentro de `WebApp`; `GET`, `HEAD` y `PUT`
son los de la web). Decisiones tomadas al implementar:

- `OPTIONS` responde siempre (sin usuario) con `DAV` y `MS-Author-Via`; el resto exige el
  usuario del share como la web. Un share con `WebDav = false` responde 405 a los métodos DAV.
- La raíz y las carpetas intermedias de *locations* son colecciones virtuales de solo lectura.
- `PROPFIND` sin cabecera `Depth` se trata como profundidad 1 (lo esperan varios clientes).
- **`PUT` puede sustituir un fichero vacío aunque el share no tenga `Overwrite`:** el Explorador
  y Finder crean primero un fichero vacío (`PUT` o `LOCK` de un recurso inexistente) y después
  lo rellenan.
- **`._nombre` y `.DS_Store`** (metadatos que escribe Finder) se aceptan aunque empiecen por `.`;
  no salen en los listados. Al borrar un fichero se borra también su `._`.
- `COPY`/`MOVE` solo dentro del mismo share (si no, 502); sustituir el destino exige `Delete`;
  `Overwrite: F` responde 412. `COPY` de carpetas es recursivo y no sigue enlaces.
- `PROPPATCH` responde 200 a todo sin aplicar nada (el Explorador fija sus atributos Win32).
- **Probado con el redirector WebDAV de Windows** (`\\equipo@puerto\DavWWWRoot`): listar,
  leer, copiar un fichero (PUT vacío, LOCK, PUT, PROPPATCH, UNLOCK), crear carpeta y renombrar.

### Clientes y limitaciones conocidas

| Cliente | Notas |
|---|---|
| Explorador de Windows 10/11 (WebClient) | Por defecto **no envía Basic sin HTTPS**: solo funcionan los shares públicos, salvo cambiando `BasicAuthLevel` en el registro. Límite de fichero de ~50 MB por defecto (`FileSizeLimitInBytes`) |
| Finder de macOS | Necesita `LOCK`; crea ficheros `._*` y `.DS_Store`, que encajan con el almacenamiento AppleDouble |
| GVfs / gestores de Linux | `dav://host:puerto/location`; los servidores anunciados por DNS-SD aparecen en "Red" |
| Carpetas web de Windows 98/2000 | A probar |
| Clientes WebDAV de Mac OS 9 | A probar |
| Otro Sunfish | Explorador integrado en las aplicaciones (ver abajo) |

## De Sunfish a Sunfish

No se añade un protocolo propio: un Sunfish explora a otro **por WebDAV**, igual que lo haría
un gestor de ficheros. Así funciona también con cualquier otro servidor WebDAV.

- **Cliente WebDAV en `Sunfish.Core`:** `PROPFIND`, `GET`, `PUT`, `MKCOL`, `MOVE` y `DELETE` con
  `HttpClient`. Las aplicaciones lo usan para un panel "Explorar otro servidor" con copia en los
  dos sentidos (ver [APLICACIONES.md](APLICACIONES.md)).
- **Descubrimiento en la LAN con DNS-SD sobre mDNS:** cada Sunfish se anuncia como
  `_http._tcp`, `_webdav._tcp` y `_ftp._tcp`, con un registro TXT `sunfish=3` y `path=/`.
  - Las aplicaciones Sunfish listan los servidores de la red sin escribir IPs.
  - Los gestores de ficheros que exploran DNS-SD (GVfs, por ejemplo) los ven solos.
  - El Explorador de Windows no explora DNS-SD: ahí hay que escribir la dirección.
- **mDNS propio** (UDP 5353 multicast, con `ReuseAddress` para convivir con Avahi, Bonjour o el
  mDNS de Windows). Se puede desactivar con `Announce = false` en `[Server]`.

**Implementación:** `Dav/DavClient.cs` (cliente), `Net/DnsMessage.cs` (mensajes DNS) y
`Net/Discovery.cs` (`MdnsAnnouncer` y `Discovery.BrowseAsync`).

- El anunciante se llama `<Name> (<equipo>)` y usa el nombre de host `sunfish-<equipo>.local`
  (para no pisar el del sistema). Anuncia al arrancar y responde a las preguntas; si no puede
  abrir el 5353, lo anota en el registro y sigue sin anunciar.
- La búsqueda pregunta desde un puerto cualquiera (los respondedores contestan a ese puerto,
  "legacy unicast"), así que funciona aunque el 5353 esté ocupado.
- Probado en Windows: convive con el mDNS del sistema y un Sunfish se encuentra a sí mismo.
  **Pendiente:** comprobarlo con Avahi (Linux) y Bonjour (macOS).
- `sunfish-cli` publicado con AOT pasa de ~4 MB a ~8 MB al incluir `HttpClient` y XML (cliente
  WebDAV).
