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>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# Vhost de la FACADE — le nginx en frontal, qui porte le TLS et relaie vers le
|
||||
# conteneur « li ». A adapter : nom, chemins du certificat, et le resolveur
|
||||
# si la facade ne tourne pas dans Docker.
|
||||
#
|
||||
# Aucune authentification ici : le laissez-passer voyage dans l URL, et il est
|
||||
# verifie par le conteneur « li », seul a connaitre les secrets. Un portail
|
||||
# d authentification (auth_request, SSO…) serait de toute facon inutilisable :
|
||||
# VLC ne suit pas une redirection vers une page de connexion.
|
||||
|
||||
# Le format « combined » par defaut journalise $request, donc la ligne de
|
||||
# requete ENTIERE, donc la signature. Ce vhost a donc son propre format : la
|
||||
# facade ne doit pas consigner ce que le conteneur « li » prend soin de taire.
|
||||
# Un journal qui porte le laissez-passer est un journal rejouable.
|
||||
log_format li_edge '$remote_addr "$arg_u" "$uri" $status $body_bytes_sent $request_time';
|
||||
|
||||
# Resolveur interne de Docker : indispensable des qu un proxy_pass porte une
|
||||
# variable. Sans lui, nginx resout l amont UNE SEULE FOIS au demarrage, et un
|
||||
# conteneur qui redemarre avec une autre adresse produit un 502 permanent.
|
||||
# A retirer si ce resolveur est deja declare ailleurs dans le contexte http.
|
||||
resolver 127.0.0.11 valid=30s ipv6=off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name li.example.org;
|
||||
location / { return 301 https://$host$request_uri; }
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name li.example.org;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/li.example.org/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/li.example.org/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_session_tickets off;
|
||||
|
||||
access_log /dev/stdout li_edge;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
# ECRASE, jamais complete : c est ce qui rend X-Real-IP digne de la
|
||||
# confiance que lui accorde le conteneur « li ».
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Sans « proxy_buffering off », nginx accumulerait le .mkv avant d en
|
||||
# relayer le premier octet. La lecture ne demarrerait jamais.
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 600s;
|
||||
send_timeout 600s;
|
||||
|
||||
set $upstream http://li:80;
|
||||
proxy_pass $upstream;
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
# Vhost du conteneur « li » — un nginx DEDIE, qui ne fait que verifier des
|
||||
# signatures et servir des fichiers. Il est derriere une facade (voir
|
||||
# facade.conf.example) qui porte le TLS.
|
||||
#
|
||||
# Chemins vus DANS le conteneur (voir compose.yaml) :
|
||||
# /etc/nginx/li/users.conf la map des ayants droit, engendree par li-user
|
||||
# /srv/li/library/ la bibliotheque, en lecture seule
|
||||
# /srv/li/playlists/ les .xspf et .html engendres par li-gen
|
||||
|
||||
# La table des ayants droit : « map $arg_u $li_secret ». Engendree par
|
||||
# li-user, jamais editee a la main.
|
||||
#
|
||||
# Son « default "" » est le coeur du dispositif : un identifiant inconnu ou
|
||||
# revoque n obtient pas un secret par defaut devinable, il n obtient RIEN, et
|
||||
# la requete est refusee avant meme le calcul de signature.
|
||||
include /etc/nginx/li/users.conf;
|
||||
|
||||
# L ADRESSE REELLE DU CLIENT, et non celle du proxy de facade.
|
||||
#
|
||||
# Sans ceci, $remote_addr vaut l adresse de la facade sur le reseau Docker :
|
||||
# toutes les lectures, tous les 403 et toute la limitation de debit seraient
|
||||
# imputes a un seul et meme voisin.
|
||||
#
|
||||
# La confiance porte sur les plages privees et non sur le sous-reseau du
|
||||
# jour : il est ATTRIBUE par Docker, et un reseau recree peut en changer. Une
|
||||
# plage figee ferait alors silencieusement reapparaitre l adresse du proxy
|
||||
# dans le journal — exactement le genre de panne qui ne se voit pas. Le risque
|
||||
# est nul tant que le reseau est « internal » et que la facade y est le seul
|
||||
# voisin de ce conteneur.
|
||||
#
|
||||
# X-Real-IP n est pas falsifiable : la facade l ECRASE a chaque requete avec
|
||||
# le $remote_addr qu elle constate, et c est elle qui est en frontal.
|
||||
set_real_ip_from 10.0.0.0/8;
|
||||
set_real_ip_from 172.16.0.0/12;
|
||||
set_real_ip_from 192.168.0.0/16;
|
||||
real_ip_header X-Real-IP;
|
||||
|
||||
# Une zone par ayant droit, et non par adresse : trois flux simultanes depuis
|
||||
# trois pays, c est du partage de lien, et cela se voit.
|
||||
limit_conn_zone $arg_u zone=li_ayants:1m;
|
||||
|
||||
# Le journal d ACCES ne contient jamais $request_uri. La signature EST le
|
||||
# laissez-passer : un journal qui la porte est un journal rejouable, et il est
|
||||
# lu par plus de monde que la table des secrets. $uri est le chemin decode,
|
||||
# sans les arguments. nginx echappe lui-meme les caracteres speciaux de $arg_u.
|
||||
#
|
||||
# Le journal d ERREUR, lui, porte la ligne de requete entiere — donc la
|
||||
# signature — et nginx ne sait pas l en priver. Le cas est etroit : une
|
||||
# signature VALIDE dont le fichier a disparu. Il sort sur stderr, donc dans
|
||||
# les journaux Docker, lisibles du seul root de l hote.
|
||||
log_format li '$remote_addr "$arg_u" "$uri" $status $body_bytes_sent $request_time';
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /srv/li;
|
||||
autoindex off;
|
||||
access_log /dev/stdout li;
|
||||
|
||||
# « hash,expiration » lus dans les arguments de l URL. La signature porte
|
||||
# sur $uri, c est-a-dire le chemin DECODE : le generateur signe donc le
|
||||
# chemin brut et n encode qu au moment d ecrire l URL. Une signature
|
||||
# calculee sur la forme encodee donne un 403 sur tout titre accentue.
|
||||
secure_link $arg_s,$arg_e;
|
||||
secure_link_md5 "$secure_link_expires$uri$li_secret";
|
||||
|
||||
# Ces trois controles sont au niveau server et non dans un location : la
|
||||
# phase de reecriture s execute AVANT le choix du location, ils couvrent
|
||||
# donc les deux arborescences sans etre ecrits deux fois. « if » suivi de
|
||||
# « return » est la seule forme que la documentation nginx ne deconseille
|
||||
# pas.
|
||||
if ($li_secret = "") { return 403; } # inconnu, ou revoque
|
||||
if ($secure_link = "") { return 403; } # signature fausse ou absente
|
||||
if ($secure_link = "0") { return 410; } # lien perime
|
||||
|
||||
limit_conn li_ayants 3;
|
||||
limit_rate_after 16m; # les premiers 16 Mo a pleine vitesse : demarrage
|
||||
limit_rate 12m; # et avance rapide restent francs
|
||||
sendfile_max_chunk 512k;
|
||||
|
||||
add_header Cache-Control "private, no-store" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
|
||||
location /library/ { }
|
||||
|
||||
# Le sous-arbre « software » se TELECHARGE, il ne s affiche pas.
|
||||
#
|
||||
# On y depose du .txt, du .html, du .svg — que le navigateur rendrait
|
||||
# volontiers DANS la page, sur l origine de la bibliotheque. Ce serveur ne
|
||||
# pose aucun cookie et n a pas de session a voler. Mais un depot de
|
||||
# fichiers qui execute ce qu on y met est une surprise qu on s epargne
|
||||
# pour trois lignes.
|
||||
#
|
||||
# Ce chemin doit rester d accord avec LI_HTML_ONLY (li.env) : un
|
||||
# repertoire de plus la-bas, c est un location de plus ici.
|
||||
location /library/software/ {
|
||||
# « add_header » dans un location REMPLACE ceux du bloc parent au lieu
|
||||
# de s y ajouter. Les deux en-tetes du niveau server sont donc repris
|
||||
# ici : sans cela, ce sous-arbre perdrait son « no-store ».
|
||||
add_header Cache-Control "private, no-store" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Content-Disposition "attachment" always;
|
||||
}
|
||||
|
||||
# Ce repertoire contient DEUX documents par ayant droit : sa playlist
|
||||
# <nom>.xspf et sa page <nom>.html. Ils portent exactement les memes URL
|
||||
# signees — la signature ne depend que du chemin et du secret, jamais du
|
||||
# document qui la transporte.
|
||||
#
|
||||
# Et ils se typent tout seuls, chacun par une voie differente :
|
||||
# .html — connu de mime.types, donc servi en text/html sans rien faire ;
|
||||
# .xspf — inconnu de mime.types, donc rattrape par le default_type.
|
||||
#
|
||||
# D ou « default_type » et NON un bloc « types » : « types » REMPLACE la
|
||||
# table heritee de mime.types au lieu de s y ajouter. Un bloc « types » ici
|
||||
# ferait perdre au .html son type — et aux .mkv le leur.
|
||||
location /playlists/ { default_type application/xspf+xml; }
|
||||
|
||||
location / { return 404; }
|
||||
}
|
||||
Reference in New Issue
Block a user