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>
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://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
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
# 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
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(softwarepar défaut), car un.ison'a rien à faire dans une playlist VLC. Chacun de ces répertoires doit avoir sonlocationavecContent-Disposition: attachmentdansnginx/li.conf, pour qu'un.htmlou un.svgdé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
python3 -m unittest discover tests
Licence
MIT.