From 608164dfdd272599ae1fc32037c6ac578ad78620 Mon Sep 17 00:00:00 2001 From: Bram Kelchtermans Date: Wed, 4 Nov 2026 10:56:55 +0100 Subject: [PATCH] multiple urls per entry --- .../plans/1793785888951-upstream-url-lists.md | 100 ++++++++++++++++++ Dockers/xtream-proxy/package.json | 2 +- Dockers/xtream-proxy/src/config.js | 62 ++++++++--- Dockers/xtream-proxy/src/settings.js | 2 +- Dockers/xtream-proxy/src/upstreams.js | 2 +- Dockers/xtream-proxy/upstreams.json.example | 16 ++- 6 files changed, 155 insertions(+), 29 deletions(-) create mode 100644 .kilo/plans/1793785888951-upstream-url-lists.md diff --git a/.kilo/plans/1793785888951-upstream-url-lists.md b/.kilo/plans/1793785888951-upstream-url-lists.md new file mode 100644 index 0000000..d958878 --- /dev/null +++ b/.kilo/plans/1793785888951-upstream-url-lists.md @@ -0,0 +1,100 @@ +# Xtream proxy: meerdere URLs per upstream-entry + +## Doel + +Breid `upstreams.json` uit zodat elke entry een lijst van mirror-URLs kan bevatten. Alle URLs binnen een entry delen dezelfde `username`/`password`. Elke URL wordt zelfstandig geprobed en globaal gerankt; bij falen wordt naar elke andere beschikbare URL geprobeerd. + +## Beslissingen + +- Configformaat wordt **breaking**: alleen `urls` (array), geen `url` meer. +- Eén entry heeft één `username` en `password`; alle URLs in `urls` gebruiken die credentials. +- Elke URL wordt een afzonderlijke `Upstream` (zelfde model als nu). Er komt geen aparte entity-laag in de runtime. +- Selectie en failover blijven globaal over alle URLs, via de bestaande `selectUpstream()` / retry-logica. +- Deduplicatie blijft op basis van de bestaande key: `origin + basePath | username | password`. +- Versie wordt opgehoogd naar `3.0.0` vanwege de breaking configwijziging. + +Voorbeeldconfig: + +```json +[ + { + "urls": [ + "http://panel1.example.com:8080", + "https://panel1.example.com" + ], + "username": "user1", + "password": "pass1" + }, + { + "urls": ["https://panel2.example.com"], + "username": "user2", + "password": "pass2" + } +] +``` + +## Implementatietaken + +1. **`src/config.js`** + - Lees per entry `urls` in plaats van `url`. + - Valideer dat `urls` een niet-lege array is; anders entry overslaan met duidelijke waarschuwing. + - Valideer `username` en `password` ongewijzigd als nu. + - Valideer elke URL afzonderlijk: + - moet een niet-lege string zijn; + - trim de waarde vóór opslaan/parsen; + - moet met `new URL()` parsebaar zijn; + - protocol moet `http:` of `https:` zijn. + - Ongeldige URL: alleen die URL overslaan met waarschuwing (entry-nummer + URL-index), niet de hele entry. + - Entry met nul geldige URLs: entry overslaan met waarschuwing. + - Als `url` aanwezig is zonder `urls`, waarschuw expliciet dat `url` niet meer wordt ondersteund. + - Vouw de geldige URLs uit naar losse entries `{ url, username, password }`, zodat de rest van de code ongewijzigd blijft. + - Behoud bestaande deduplicatie en state-preservatie in `reloadConfig()` op basis van `Upstream.key`. + +2. **`src/upstreams.js`** + - Geen selectie- of probewijziging nodig: de uitgevouwen URLs gedragen zich als bestaande upstreams. + - Pas eventueel logteksten aan zodat aantallen en resultaten duidelijk over URLs/mirrors gaan in plaats van entries. + +3. **`upstreams.json.example`** + - Vervang `url` door `urls`-arrays. + - Maak het voorbeeld geldige JSON (de huidige `#`-commentaarregels zijn geen geldige JSON). + - Documenteer in de voorbeeldtekst/naamgeving dat alle URLs dezelfde credentials delen en onafhankelijk worden geprobed. + +4. **Versie en user-agent** + - `package.json`: `version` naar `3.0.0`. + - `src/settings.js`: standaard `USER_AGENT` naar `xtream-proxy/3.0`. + - `Dockerfile` hoeft functioneel niet te wijzigen; controleer alleen of er geen versies/verwijzingen naar het oude formaat in staan. + +5. **Documentatie/commentaar in code** + - Werk comments bij die nog spreken over één URL per upstream. + +## Niet in scope + +- Per-URL afwijkende credentials. +- Een naam/label per entry of per mirror. +- Prioriteit/weight per mirror; selectie blijft op healthy + busy + latency. +- Wijzigingen aan de stream-rewrite of load-tracking, anders dan dat elke mirror zijn eigen state krijgt. + +## Risico's en aandachtspunten + +- **Breaking change:** bestaande configs met `url` werken niet meer. Duidelijk waarschuwen bij het inladen en bijbehorende aanpassing van de voorbeeldconfig opnemen. +- **Aantallen in logs/health:** `getUpstreams()` bevat nu één item per mirror-URL, niet per entry. Dit is bewust; pas teksten aan waar nodig. +- **Dubbele labels:** dezelfde URL met verschillende credentials levert twee upstreams met hetzelfde label op in logs/health. Dit bestond al voor losse entries en blijft buiten scope. +- **Gedeelde accountlimieten:** als mirrors dezelfde account/panel-limiet delen, kan de proxy per URL apart tellen. De bestaande `max_connections`/`active_cons`-probe blijft leidend per URL. + +## Validatie + +1. `node --check server.js` en `node --check` op alle gewijzigde bestanden in `src/`. +2. Start de proxy met een tijdelijke config die: + - één entry met twee geldige URLs bevat; + - één entry met één geldige URL bevat; + - één ongeldige URL tussen geldige URLs bevat; + - één entry met alleen `url` bevat (moet gewaarschuwd worden overgeslagen). +3. Controleer in de logs: + - het juiste aantal upstreams/URLs; + - waarschuwingen voor de ongeldige URL en de oude `url`-entry; + - probe-resultaten per URL. +4. Controleer `/health`: + - elke mirror-URL verschijnt als aparte upstream; + - `ok`, `selected` en `upstreams` blijven geldig. +5. Test failover handmatig door één mirror onbereikbaar te maken en te verifiëren dat een request via een andere URL slaagt. +6. Test deduplicatie: dezelfde URL + credentials twee keer in de config levert één upstream op. diff --git a/Dockers/xtream-proxy/package.json b/Dockers/xtream-proxy/package.json index b8c3098..beef367 100644 --- a/Dockers/xtream-proxy/package.json +++ b/Dockers/xtream-proxy/package.json @@ -1,6 +1,6 @@ { "name": "xtream-proxy", - "version": "2.0.0", + "version": "3.0.0", "private": true, "description": "Proxies the fastest available upstream from a list of Xtream Codes credentials", "scripts": { diff --git a/Dockers/xtream-proxy/src/config.js b/Dockers/xtream-proxy/src/config.js index 3790267..4678af2 100644 --- a/Dockers/xtream-proxy/src/config.js +++ b/Dockers/xtream-proxy/src/config.js @@ -20,37 +20,65 @@ function readConfigEntries() { const entries = []; data.forEach((item, index) => { + const label = `Config entry #${index + 1}`; if (!item || typeof item !== 'object' || Array.isArray(item)) { - console.warn(`Config entry #${index + 1}: not an object, skipping`); + console.warn(`${label}: not an object, skipping`); return; } - const { url, username, password } = item; - if (typeof url !== 'string' || !url.trim()) { - console.warn(`Config entry #${index + 1}: missing url, skipping`); + + if (item.url !== undefined) { + console.warn(`${label}: "url" is no longer supported and is ignored, use "urls"`); + } + + const { urls, username, password } = item; + if (!Array.isArray(urls) || urls.length === 0) { + console.warn(`${label}: missing non-empty urls array, skipping`); return; } if (typeof username !== 'string' || !username) { - console.warn(`Config entry #${index + 1}: missing username, skipping`); + console.warn(`${label}: missing username, skipping`); return; } if (typeof password !== 'string' || !password) { - console.warn(`Config entry #${index + 1}: missing password, skipping`); + console.warn(`${label}: missing password, skipping`); return; } - let parsed; - try { - parsed = new URL(url); - } catch { - console.warn(`Config entry #${index + 1}: invalid url, skipping`); - return; - } - if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { - console.warn(`Config entry #${index + 1}: url must be http(s), skipping`); + const validUrls = []; + const seenUrls = new Set(); + urls.forEach((rawUrl, urlIndex) => { + const urlLabel = `${label} url #${urlIndex + 1}`; + if (typeof rawUrl !== 'string' || !rawUrl.trim()) { + console.warn(`${urlLabel}: missing url, skipping`); + return; + } + + const url = rawUrl.trim(); + let parsed; + try { + parsed = new URL(url); + } catch { + console.warn(`${urlLabel}: invalid url, skipping`); + return; + } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + console.warn(`${urlLabel}: url must be http(s), skipping`); + return; + } + if (seenUrls.has(url)) return; + + seenUrls.add(url); + validUrls.push(url); + }); + + if (validUrls.length === 0) { + console.warn(`${label}: no valid urls, skipping`); return; } - entries.push({ url, username, password }); + for (const url of validUrls) { + entries.push({ url, username, password }); + } }); return entries; } @@ -78,7 +106,7 @@ function reloadConfig() { const removed = upstreams.filter((u) => !seen.has(u.key)).length; const added = next.filter((u) => !previous.has(u.key)).length; if (added || removed) { - console.log(`Config reloaded: ${next.length} upstream(s), +${added} / -${removed}`); + console.log(`Config reloaded: ${next.length} upstream URL(s), +${added} / -${removed}`); } upstreams = next; diff --git a/Dockers/xtream-proxy/src/settings.js b/Dockers/xtream-proxy/src/settings.js index dbc68f9..88b8d6f 100644 --- a/Dockers/xtream-proxy/src/settings.js +++ b/Dockers/xtream-proxy/src/settings.js @@ -21,7 +21,7 @@ const settings = { ), rejectUnauthorized: process.env.PROXY_INSECURE_TLS !== '1', allowBusyFallback: process.env.ALLOW_BUSY_FALLBACK !== '0', - userAgent: process.env.USER_AGENT || 'xtream-proxy/2.0', + userAgent: process.env.USER_AGENT || 'xtream-proxy/3.0', }; function validate() { diff --git a/Dockers/xtream-proxy/src/upstreams.js b/Dockers/xtream-proxy/src/upstreams.js index 083ac26..76520f4 100644 --- a/Dockers/xtream-proxy/src/upstreams.js +++ b/Dockers/xtream-proxy/src/upstreams.js @@ -70,7 +70,7 @@ function describeUpstream(upstream) { function logProbeResults(snapshot) { if (snapshot.length === 0) { - console.warn(`No upstreams configured in ${settings.configFile}`); + console.warn(`No upstream URLs configured in ${settings.configFile}`); return; } diff --git a/Dockers/xtream-proxy/upstreams.json.example b/Dockers/xtream-proxy/upstreams.json.example index 972080e..467f1a8 100644 --- a/Dockers/xtream-proxy/upstreams.json.example +++ b/Dockers/xtream-proxy/upstreams.json.example @@ -1,18 +1,16 @@ -# -# Every entry must point to an Xtream-compatible panel that serves the same -# content. The proxy probes each entry via /player_api.php and picks the -# fastest one that is not at its connection limit. -# -# The URL may include a path prefix, e.g. https://example.com/portal -# [ { - "url": "http://panel1.example.com:8080", + "urls": [ + "http://panel1.example.com:8080", + "https://panel1.example.com" + ], "username": "user1", "password": "pass1" }, { - "url": "https://panel2.example.com", + "urls": [ + "https://panel2.example.com" + ], "username": "user2", "password": "pass2" }