Files
k3nnyandClaude Opus 5.5 1e63e4fe80 Bibliothèque vidéo partagée par URL signées
Extrait de li.k3nny.fr et rendu indépendant du serveur : toute la
configuration propre à l'hôte passe par li.env (URL publique, chemins,
expiration, répertoires « HTML seulement », commande nginx).

Écarts avec la version en production :
- li-user del retire aussi la page .html, pas seulement le .xspf ;
- li-gen ignore les répertoires cachés, pas seulement les fichiers ;
- textes de la page HTML accentués.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 10:06:08 +02:00

165 lines
7.3 KiB
Markdown

# li — une bibliothèque vidéo partagée par URL signées
Partager une bibliothèque de films et de séries avec quelques proches, **lisible directement dans
VLC**, sans compte, sans portail d'authentification et sans secret partagé.
Chaque personne reçoit **deux liens personnels** :
| Document | Pour quoi faire |
|---|---|
| `<nom>.xspf` | Une playlist VLC contenant toute la bibliothèque (*Média → Ouvrir un flux réseau*) |
| `<nom>.html` | Une page autonome : l'arborescence, les tailles, un filtre par titre, un clic pour télécharger |
Les deux portent **exactement les mêmes URL de téléchargement**, puisque la signature ne dépend que
du chemin et du secret de la personne, jamais du document qui la transporte.
## Le principe : la révocation, c'est la table
Chaque ayant droit possède un identifiant et un secret de 24 caractères, qui **ne quitte jamais le
serveur**. Une URL porte le nom, une expiration et un `md5(expiration + chemin + secret)`, que
nginx vérifie lui-même avec [`secure_link`](https://nginx.org/en/docs/http/ngx_http_secure_link_module.html) :
```
https://li.example.org/library/movies/Film.mkv?u=alice&e=2145916800&s=O8H1HbrY_SGVLnl8KP-AxA
```
| Levier | Rôle |
|---|---|
| `li-user del alice` | **La** révocation : tous les liens de la personne meurent dans la seconde |
| L'expiration `e=` | Un filet, rien de plus — fixée à 2038 par défaut |
Si l'expiration est aussi lointaine, c'est parce que VLC indexe la reprise de lecture et
l'historique **par URL** : une signature renouvelée chaque nuit les effacerait chaque nuit. Ici,
les URL ne changent jamais, et la régénération nocturne ne fait qu'ajouter les nouveautés.
Ce choix permet aussi de se passer d'un portail d'authentification (SSO, `auth_request`). VLC ne
suit pas une redirection vers une page de connexion, et une sous-requête d'authentification
répétée à chaque requête de plage (chaque avance rapide) coûte cher. Un jeton signé se vérifie sur
place, en quelques microsecondes.
## Architecture
```mermaid
flowchart LR
AMI(["Ami · VLC ou navigateur"])
NGX["<b>nginx</b> · façade<br/><i>443, TLS, X-Real-IP</i>"]
subgraph NET["réseau Docker « li » · internal"]
LI["<b>li</b> — nginx dédié<br/><i>vérifie la signature</i>"]
end
MAP[("users.conf<br/><i>map $arg_u → secret</i>")]
LIB[("bibliothèque<br/><b>lecture seule</b>")]
AMI -->|"https · URL signée"| NGX
NGX -->|"proxy_pass<br/>proxy_buffering off"| LI
LI --> MAP
LI --> LIB
```
Le nginx de façade ne monte pas la bibliothèque : c'est le processus le plus exposé de la machine,
et lui donner accès à des centaines de gigaoctets élargirait son rayon d'explosion pour rien. Le
conteneur `li` ne publie aucun port, ne joint personne (réseau `internal`) et voit la bibliothèque
en lecture seule.
## Contenu du dépôt
| Fichier | Rôle |
|---|---|
| `bin/li-user` | Ajoute, révoque et liste les ayants droit, et affiche leurs liens (sh, `jq`, `openssl`) |
| `bin/li-gen` | Engendre la playlist et la page de chaque ayant droit (Python ≥ 3.8, sans dépendance) |
| `nginx/li.conf` | Vhost du conteneur `li` : signature, limites, en-têtes |
| `nginx/facade.conf.example` | Vhost de la façade TLS, à adapter |
| `compose.yaml` | Le conteneur `li` et son réseau |
| `systemd/li-gen.*` | Régénération nocturne |
| `li.env.example` | **Toute** la configuration propre au serveur |
## Installation
```bash
# 1. Configuration
sudo install -d /etc/li
sudo install -m 644 li.env.example /etc/li/li.env
sudoedit /etc/li/li.env # LI_BASE_URL, LI_LIBRARY, LI_DATA…
# 2. Scripts et minuteur
sudo install -m 755 bin/li-gen bin/li-user /usr/local/sbin/
sudo install -m 644 systemd/li-gen.service systemd/li-gen.timer /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now li-gen.timer
# 3. Répertoires de données, avec une map vide pour que nginx démarre
. /etc/li/li.env
sudo install -d "$LI_DATA/conf" "$LI_DATA/playlists"
echo 'map $arg_u $li_secret { default ""; }' | sudo tee "$LI_DATA/conf/users.conf"
# 4. Le conteneur
sudo docker compose --env-file /etc/li/li.env up -d
```
Côté façade, adaptez `nginx/facade.conf.example` (nom, certificat) et raccordez la façade au
réseau `li` (`networks: li: {external: true}` dans son `compose`).
Hors Docker, `nginx/li.conf` s'utilise tel quel une fois ses chemins adaptés (`root`, `include`),
avec `LI_NGINX=nginx` dans `li.env`.
## Utilisation
```bash
li-user add alice # crée, engendre la map, recharge nginx, affiche les deux liens
li-user lien alice # retrouve les deux liens, sans rien changer
li-user list
li-user del alice # tue tous ses liens dans la seconde
li-gen # régénère à la main (le minuteur le fait chaque nuit)
```
Les liens **sont** les clés : transmettez-les par un canal privé (un envoi à usage unique d'un
gestionnaire de mots de passe convient), pas dans une discussion de groupe.
### Ce qui va dans la playlist
* Partout : les fichiers dont l'extension figure dans `LI_VIDEO_EXT`.
* Dans la page HTML seulement : **tous** les fichiers des répertoires de premier niveau listés dans
`LI_HTML_ONLY` (`software` par défaut), car un `.iso` n'a rien à faire dans une playlist VLC.
Chacun de ces répertoires doit avoir son `location` avec `Content-Disposition: attachment` dans
`nginx/li.conf`, pour qu'un `.html` ou un `.svg` déposé là soit téléchargé et non rendu.
* Les fichiers et répertoires cachés (`.Trash-1000`…) sont ignorés.
### Limites côté lecteur
| Limite | Pourquoi |
|---|---|
| Fichiers lus **tels quels**, sans transcodage | Un 4K HEVC ne passera pas sur un vieux téléphone |
| Les sous-titres intégrés au `.mkv` fonctionnent, les `.srt` voisins non | VLC ne cherche les sous-titres voisins que pour les fichiers locaux |
| Trois flux simultanés par personne | `limit_conn` par identifiant, et non par adresse |
| 12 Mo/s par flux, les 16 premiers Mo à pleine vitesse | Démarrage et avance rapide restent francs |
## Pièges connus
**Monter le répertoire, pas le fichier.** `li-user` publie la map par un `mv` atomique, qui
remplace l'inode. Un bind-mount *de fichier* épingle l'inode d'origine : le conteneur resterait sur
l'ancienne version, et un ayant droit ajouté n'existerait jamais pour nginx, sans la moindre
erreur.
**Signer le chemin décodé.** `secure_link_md5` porte sur `$uri`, que nginx a déjà décodé. On signe
donc le chemin brut et on n'encode qu'au moment d'écrire l'URL. Dans l'autre ordre, on obtient un
`403` sur tout titre qui contient une espace ou un accent. De même, les `&` doivent devenir `&amp;`
dans le XML, sans quoi VLC refuse le `.xspf`.
**Ne pas journaliser la signature.** Le format `combined` enregistre la ligne de requête entière,
et donc le laissez-passer. Les deux nginx (façade et `li`) utilisent un format qui ne retient que
`$uri`. Seul le journal d'erreurs porte encore la requête complète, dans un cas étroit (signature
valide mais fichier disparu).
**L'adresse du client.** Sans `realip`, le conteneur attribuerait toutes les requêtes à l'adresse
de la façade. Il fait confiance à `X-Real-IP` depuis les plages privées, et non depuis un
sous-réseau figé que Docker peut réattribuer. En contrepartie, la façade doit **écraser** cet
en-tête.
## Tests
```bash
python3 -m unittest discover tests
```
## Licence
[MIT](LICENSE).