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

7.3 KiB

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 (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

python3 -m unittest discover tests

Licence

MIT.