This commit is contained in:
1 parent
4bd2088edd
commit
608164dfdd
6 files changed
+155
-29
No files matched your search
@@ -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.
|
||||
@@ -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": {
|
||||
|
||||
@@ -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`);
|
||||
return;
|
||||
}
|
||||
const { url, username, password } = item;
|
||||
if (typeof url !== 'string' || !url.trim()) {
|
||||
console.warn(`Config entry #${index + 1}: missing url, skipping`);
|
||||
return;
|
||||
}
|
||||
if (typeof username !== 'string' || !username) {
|
||||
console.warn(`Config entry #${index + 1}: missing username, skipping`);
|
||||
return;
|
||||
}
|
||||
if (typeof password !== 'string' || !password) {
|
||||
console.warn(`Config entry #${index + 1}: missing password, skipping`);
|
||||
console.warn(`${label}: not an object, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
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(`${label}: missing username, skipping`);
|
||||
return;
|
||||
}
|
||||
if (typeof password !== 'string' || !password) {
|
||||
console.warn(`${label}: missing password, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
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(`Config entry #${index + 1}: invalid url, skipping`);
|
||||
console.warn(`${urlLabel}: invalid url, skipping`);
|
||||
return;
|
||||
}
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
console.warn(`Config entry #${index + 1}: url must be http(s), skipping`);
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user