Fix: URL de manifest fiable + champ toujours visible et copiable
Some checks failed
Build & Push Docker image / build (push) Has been cancelled

Un utilisateur a signalé "unable to parse TLS packet header" en installant
via le bouton, et un bouton "Copier" qui ne fait rien sur Brave/Android.

Cause racine commune : /configure construisait l'URL du manifest et le lien
stremio:// côté client via window.location.protocol/host — fiable
uniquement si la page /configure elle-même a été chargée par le même chemin
que celui utilisé ensuite pour installer l'addon. Si un utilisateur atteint
le VPS par un chemin différent (accès direct au port, proxy sans
X-Forwarded-Proto...), la page peut se charger en http alors que le domaine
public est en https (ou l'inverse) : le lien stremio:// généré promet un
schéma que le serveur ne tient pas à cette adresse, d'où l'erreur TLS.

- Le serveur injecte désormais PUBLIC_BASE (calculé via external_host_url,
  la même logique fiable X-Forwarded-Proto/RANGER_PUBLIC_URL qui sert déjà
  au manifest JSON) dans la page /configure, échappé comme PREFILL. Le JS
  ne déduit plus jamais le schéma/host depuis window.location.
- navigator.clipboard exige un contexte sécurisé (HTTPS) : sur http://, il
  est absent (pas juste refusé), ce qui expliquait le bouton "Copier"
  silencieusement inopérant. copyUrl() dégrade maintenant vers
  document.execCommand('copy') (fonctionne aussi en http), puis en dernier
  recours sélectionne le texte du champ pour une copie manuelle.
- Le champ URL du manifest est désormais TOUJOURS visible (readonly,
  sélectionnable au clic) au lieu d'être cascadé derrière un clic sur
  "Copier" — décorrèle la consultation/copie manuelle du bouton d'action,
  qui reste utilisable indépendamment pour l'installation en un clic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Valentin 2026-07-20 11:17:24 +02:00
parent 6e6703d012
commit 51130931f4
2 changed files with 58 additions and 26 deletions

View file

@ -150,6 +150,15 @@ async def handle_configure(request):
prefill = json_for_script(decoded)
content = content.replace("const PREFILL = null;", f"const PREFILL = {prefill};")
# Base publique fiable (schéma+host), calculée côté serveur — la page
# ne doit pas déduire le schéma via window.location côté client : si
# /configure a été atteinte par un chemin http (proxy, accès direct
# au port...), l'URL de manifest générée hériterait du mauvais
# schéma, causant une erreur TLS à l'installation dans Stremio.
content = content.replace(
'const PUBLIC_BASE = "";', f'const PUBLIC_BASE = {json_for_script(external_host_url(request))};'
)
return web.Response(text=content, content_type="text/html")
except Exception as e:
logging.error(f"configure error: {e}")

View file

@ -67,11 +67,12 @@
padding: 14px; border-radius: 10px; text-decoration: none; font-size: 1rem;
}
.install.disabled { opacity: .4; pointer-events: none; }
.url-box {
background: var(--card2); border: 1px solid var(--border); border-radius: 8px; padding: 10px;
font-size: .78rem; word-break: break-all; color: var(--muted); margin-top: 8px; display: none;
max-height: 4.6em; overflow-y: auto;
.url-row { display: flex; gap: 8px; }
.url-field {
flex: 1; min-width: 0; background: var(--card2); border: 1px solid var(--border); border-radius: 8px;
padding: 10px 12px; font-size: .78rem; color: var(--muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.url-field:focus { outline: none; border-color: var(--accent); color: var(--text); }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
padding: 7px 12px; border-radius: 20px; border: 1px solid var(--border); background: var(--card2);
@ -242,11 +243,12 @@
</div>
<div class="actions">
<div class="url-box" id="urlBox"></div>
<p class="small muted" id="installHint" style="margin:0 0 4px;text-align:center"></p>
<div class="row">
<button class="btn ghost" onclick="copyUrl()">📋 Copier l'URL du manifest</button>
<label class="small muted" style="margin-bottom:4px">URL du manifest</label>
<div class="url-row">
<input type="text" id="urlField" class="url-field" readonly onclick="this.select()">
<button class="btn ghost sm" onclick="copyUrl()" id="copyBtn">📋 Copier</button>
</div>
<p class="small muted" id="installHint" style="margin:4px 0 4px;text-align:center"></p>
<a class="install disabled" id="installBtn"> Installer dans Stremio</a>
</div>
</div>
@ -285,6 +287,14 @@ const SOURCE_LABELS = {
const PREFILL = null;
const IS_EDITING = PREFILL !== null;
// Base publique (scheme+host) calculée côté serveur (X-Forwarded-Proto /
// RANGER_PUBLIC_URL) — fiable même si la page /configure elle-même a été
// chargée via un chemin http (proxy, accès direct au port...). Utiliser
// window.location.protocol/host ici serait faux dans ce cas : ça a déjà
// causé des URLs stremio://...https implicite pointant en réalité vers un
// serveur http, d'où des erreurs TLS côté client à l'installation.
const PUBLIC_BASE = "";
let debrids = []; // [{service, key}]
let unit3ds = []; // [{url, key}]
let torznabs = []; // [{name, url, apikey}]
@ -502,15 +512,17 @@ function b64url(obj) {
function update() {
const cfg = buildConfig();
const encoded = b64url(cfg);
const host = window.location.host;
const proto = window.location.protocol;
const manifestUrl = `${proto}//${host}/${encoded}/manifest.json`;
document.getElementById("urlBox").textContent = manifestUrl;
// Base fournie par le serveur (fiable, cf. commentaire sur PUBLIC_BASE) ;
// repli sur window.location seulement si l'injection serveur a échoué.
const base = PUBLIC_BASE || `${window.location.protocol}//${window.location.host}`;
const baseHost = base.replace(/^https?:\/\//, "");
const manifestUrl = `${base}/${encoded}/manifest.json`;
document.getElementById("urlField").value = manifestUrl;
const btn = document.getElementById("installBtn");
const valid = cfg.debrids.length > 0;
btn.classList.toggle("disabled", !valid);
btn.href = valid ? `stremio://${host}/${encoded}/manifest.json` : "#";
btn.href = valid ? `stremio://${baseHost}/${encoded}/manifest.json` : "#";
window._manifestUrl = manifestUrl;
// En mode édition (config existante rouverte), le bouton réinstalle
@ -526,22 +538,33 @@ function update() {
}
function copyUrl() {
// navigator.clipboard exige un "contexte sécurisé" (HTTPS) — indisponible
// (undefined, pas juste refusé) si la page est chargée en http://. On
// dégrade vers document.execCommand('copy') (fonctionne aussi en http),
// puis en dernier recours vers une sélection manuelle du champ (toujours
// visible désormais, donc copiable à la main dans tous les cas).
const b = event.target;
const t = b.textContent;
navigator.clipboard.writeText(window._manifestUrl || "").then(() => {
// La confirmation sur le bouton suffit ; pas besoin d'afficher l'URL
// complète (elle peut faire >1800 caractères avec une config étoffée).
b.textContent = "✅ Copié !";
setTimeout(() => b.textContent = t, 1500);
}).catch(() => {
// Fallback si le presse-papier est inaccessible (permissions navigateur) :
// on affiche l'URL dans une boîte à défilement pour copie manuelle.
const box = document.getElementById("urlBox");
box.style.display = "block";
box.textContent = window._manifestUrl || "";
b.textContent = "⚠️ Copie manuelle ci-dessous";
const field = document.getElementById("urlField");
const showCopied = () => { b.textContent = "✅ Copié !"; setTimeout(() => b.textContent = t, 1500); };
const showManual = () => {
field.focus(); field.select();
b.textContent = "☝️ Sélectionné — Ctrl/Cmd+C";
setTimeout(() => b.textContent = t, 2500);
});
};
const execCommandFallback = () => {
field.focus(); field.select();
let ok = false;
try { ok = document.execCommand("copy"); } catch (e) { ok = false; }
if (ok) showCopied(); else showManual();
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(window._manifestUrl || "").then(showCopied).catch(execCommandFallback);
} else {
execCommandFallback();
}
}
// ---- Persistance : appliquer la config pré-remplie ----