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>
165 lines
7.3 KiB
Markdown
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 `&`
|
|
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).
|