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:
@@ -99,6 +99,17 @@ export function surveiller(reference) {
|
||||
//
|
||||
// OpenLibrary envoie `Access-Control-Allow-Origin: *` (vérifié) : un fetch cross-origin normal
|
||||
// suffit, pas besoin du contournement `no-cors`/réponse opaque.
|
||||
//
|
||||
// ⚠️ Mais le formulaire livre accepte N'IMPORTE QUELLE URL de couverture, et la plupart des
|
||||
// hébergeurs n'envoient aucun en-tête CORS. Ces images s'affichaient bien — une <img> n'a que
|
||||
// faire du CORS — sans jamais pouvoir être mises en cache, et le fetch repartait à CHAQUE
|
||||
// affichage puisque rien n'était jamais rangé. D'où le repli par `/api/couvertures`, qui relaie
|
||||
// l'image en MÊME ORIGINE (voir ServiceCouvertures côté serveur, et ses deux verrous anti-SSRF).
|
||||
//
|
||||
// ⚠️ L'ordre compte : on tente d'abord l'URL directe, qui profite du cache HTTP du navigateur
|
||||
// (même URL que l'<img> déjà chargée) et n'impose rien à notre serveur. Le relais n'entre en jeu
|
||||
// que pour ce que le CORS a bloqué. Une réponse opaque (`mode: 'no-cors'`) ne conviendrait pas :
|
||||
// son corps est illisible, donc impossible à ranger en IndexedDB.
|
||||
export async function couvertureMettreEnCache(url) {
|
||||
try {
|
||||
const base = await ouvrir();
|
||||
@@ -106,8 +117,20 @@ export async function couvertureMettreEnCache(url) {
|
||||
const existe = await attendre(tx.objectStore(MAGASIN_COUVERTURES).getKey(url), tx);
|
||||
if (existe !== undefined) return; // Déjà en cache : pas de re-téléchargement.
|
||||
|
||||
const reponse = await fetch(url, { cache: 'force-cache' });
|
||||
if (!reponse.ok) return; // 404/502 intermittent (CLAUDE.md) : rien à ranger.
|
||||
let reponse = null;
|
||||
|
||||
try {
|
||||
reponse = await fetch(url, { cache: 'force-cache' });
|
||||
} catch (eDirect) {
|
||||
// Bloqué par le CORS, ou hôte injoignable : on ne sait pas lequel, et peu importe.
|
||||
}
|
||||
|
||||
if (!reponse || !reponse.ok) {
|
||||
reponse = await fetch('/api/couvertures?url=' + encodeURIComponent(url));
|
||||
}
|
||||
|
||||
// 404/502 intermittent d'OpenLibrary (CLAUDE.md), ou relais refusé : rien à ranger.
|
||||
if (!reponse.ok) return;
|
||||
|
||||
const blob = await reponse.blob();
|
||||
const ecriture = base.transaction(MAGASIN_COUVERTURES, 'readwrite');
|
||||
|
||||
Reference in New Issue
Block a user