Relaie les couvertures en même origine, pour les mettre en cache hors-ligne

Le cache hors-ligne des couvertures ne marchait que pour OpenLibrary : il lit
les octets par fetch(), donc exige un en-tête CORS, alors que le formulaire
livre accepte n'importe quelle URL. Ces images s'affichaient (une <img> n'a
que faire du CORS) sans jamais pouvoir être rangées — et le fetch repartait à
chaque affichage puisque rien n'était stocké.

GET /api/couvertures relaie l'image depuis notre serveur. Un proxy est une
surface SSRF : il est borné par deux verrous indépendants — l'URL doit déjà
exister en base comme couverture, et la connexion ne s'ouvre que vers une
adresse publiquement routable. Ce second verrou vit dans le ConnectCallback,
pas dans une pré-vérification DNS, ce qui ferme aussi le DNS rebinding — et
c'est ce qui permet de suivre les redirections, indispensables puisque
covers.openlibrary.org répond 302.

Tout refus répond 404 : distinguer les cas ferait du point d'entrée un oracle
sur les URL connues et sur le réseau du serveur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-20 22:14:08 +02:00
co-authored by Claude Opus 5
parent e74d20adfa
commit b16e70913f
7 changed files with 630 additions and 2 deletions
+27
View File
@@ -1,6 +1,7 @@
using MaBibli.Api.Data;
using MaBibli.Api.Endpoints;
using MaBibli.Api.Services.Catalogue;
using MaBibli.Api.Services.Couvertures;
using MaBibli.Api.Services.Identite;
using MaBibli.Api.Services.Isbn;
using MaBibli.Api.Services.Prets;
@@ -50,6 +51,31 @@ builder.Services.AddHttpClient<IOpenLibraryClient, OpenLibraryClient>(http =>
builder.Services.AddScoped<IIsbnLookupService, IsbnLookupService>();
// Relais de couvertures : rend une image de MÊME ORIGINE, seule façon de la mettre en cache
// hors-ligne quand son hébergeur n'envoie pas d'en-tête CORS (le formulaire livre accepte
// n'importe quelle URL de couverture).
//
// ⚠️ Les deux réglages du handler ne sont PAS des détails de confort :
// - ConnectCallback n'ouvre la socket que vers une adresse publiquement routable, ce qui
// ferme le SSRF y compris par DNS rebinding (voir GardeAdresses) ;
// - les redirections sont SUIVIES, mais bornées à 3. Elles avaient d'abord été coupées, à
// tort : `covers.openlibrary.org` répond 302 (mesuré), et beaucoup de CDN font de même —
// les couper refusait donc les couvertures les plus courantes. Les suivre reste sûr parce
// que le garde agit à la CONNEXION : il s'applique à chaque saut, cible de redirection
// comprise. C'est la position du garde, et non l'interdiction des redirections, qui ferme
// le SSRF.
builder.Services.AddHttpClient<IServiceCouvertures, ServiceCouvertures>(http =>
{
http.Timeout = timeoutSources;
http.DefaultRequestHeaders.UserAgent.ParseAdd("MaBibli/0.1 (bibliotheque personnelle auto-hebergee)");
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
AllowAutoRedirect = true,
MaxAutomaticRedirections = 3,
ConnectCallback = GardeAdresses.ConnecterAsync,
});
var app = builder.Build();
// Une seule base SQLite, migrée au démarrage : pas d'étape manuelle à l'installation YunoHost.
@@ -98,6 +124,7 @@ app.MapSeriesEndpoints();
app.MapRevuesEndpoints();
app.MapBibliographieEndpoints();
app.MapIdentiteEndpoints();
app.MapCouverturesEndpoints();
// ⚠️ Le fallback a son propre pipeline : il NE passe PAS par les StaticFileOptions posées
// ci-dessus. Sans lui repasser les mêmes options, « / » — c'est-à-dire le start_url de la PWA,