# 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 | |---|---| | `.xspf` | Une playlist VLC contenant toute la bibliothèque (*Média → Ouvrir un flux réseau*) | | `.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["nginx · façade
443, TLS, X-Real-IP"] subgraph NET["réseau Docker « li » · internal"] LI["li — nginx dédié
vérifie la signature"] end MAP[("users.conf
map $arg_u → secret")] LIB[("bibliothèque
lecture seule")] AMI -->|"https · URL signée"| NGX NGX -->|"proxy_pass
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).