@swimmingliu/autovpn 2.1.2 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config/profile.js
CHANGED
|
@@ -57,7 +57,11 @@ const OFFICIAL_SOURCE_KEYS = new Set(['leiting', 'heidong', 'mifeng', 'xuanfeng'
|
|
|
57
57
|
const OBSOLETE_SOURCE_FIELDS = [
|
|
58
58
|
'url', 'key', 'account', 'password', 'platform',
|
|
59
59
|
'use_random_area', 'area_min', 'area_max',
|
|
60
|
-
'resume_from_iteration', 'failure_limit'
|
|
60
|
+
'resume_from_iteration', 'failure_limit',
|
|
61
|
+
// Internal: the extractor sets this when `backend_brand` redirects the data
|
|
62
|
+
// plane. It is derived state, so a hand-edited profile must not persist it —
|
|
63
|
+
// it would outlive the redirect that produced it.
|
|
64
|
+
'brand_override'
|
|
61
65
|
];
|
|
62
66
|
function positiveInteger(value, fallback) {
|
|
63
67
|
const parsed = Number(value);
|
|
@@ -95,6 +99,18 @@ export function normalizeOfficialSources(sources) {
|
|
|
95
99
|
areas_per_round: Math.min(64, positiveInteger(config.areas_per_round, 8)),
|
|
96
100
|
area_concurrency: Math.min(16, positiveInteger(config.area_concurrency, 4))
|
|
97
101
|
};
|
|
102
|
+
// Only a known vendor can serve as a data plane; an unknown name would
|
|
103
|
+
// silently fall back to the row's own brand and produce the very node
|
|
104
|
+
// shortage the field exists to fix.
|
|
105
|
+
if (config.backend_brand !== undefined) {
|
|
106
|
+
const backend = normalizeBrand(String(config.backend_brand).trim());
|
|
107
|
+
if (backend && backend !== brand) {
|
|
108
|
+
normalized.backend_brand = backend;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
delete normalized.backend_brand;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
98
114
|
if (!wasCredential) {
|
|
99
115
|
// A legacy URL row's caps describe a different operation; start the
|
|
100
116
|
// cursor flow at the documented scheduled-run default instead. A floor
|
|
@@ -9,6 +9,23 @@ import { BRANDS, brandFromSite, brandSite, normalizeBrand } from '../pool/brands
|
|
|
9
9
|
import { FingerprintStore, resolveFingerprintCacheDir } from './brand-cache.js';
|
|
10
10
|
import { resolveUserRuntimeRoot } from '../runtime/paths.js';
|
|
11
11
|
import { redactText } from '../runtime/redaction.js';
|
|
12
|
+
/**
|
|
13
|
+
* How many pooled accounts one source may try before it reports failure.
|
|
14
|
+
*
|
|
15
|
+
* A redirected row leases from a foreign vendor's list, and only a minority of
|
|
16
|
+
* those accounts exist on the backend: measured against the deployed pool, 2 of
|
|
17
|
+
* 40 黑洞 accounts log in on the 雷霆 backend. Each rejected attempt retires that
|
|
18
|
+
* account and leases the next by least-recent use, so the width decides whether
|
|
19
|
+
* the run ever reaches a usable credential. A row without a redirect keeps the
|
|
20
|
+
* tighter default.
|
|
21
|
+
*/
|
|
22
|
+
export function credentialAccountAttempts(source, brand) {
|
|
23
|
+
const backend = normalizeBrand(String(source.backend_brand ?? '').trim());
|
|
24
|
+
const redirected = Boolean(backend && brand && backend !== brand);
|
|
25
|
+
const fallback = redirected ? 24 : 3;
|
|
26
|
+
const parsed = Math.trunc(Number(source.account_retry_limit ?? fallback));
|
|
27
|
+
return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
|
|
28
|
+
}
|
|
12
29
|
/**
|
|
13
30
|
* Each brand concatenates `getCommParams()` in its own order with its own
|
|
14
31
|
* literal block. The layout can be overridden from the profile, and is picked
|
|
@@ -28,7 +45,9 @@ export function resolveBrandLayout(source) {
|
|
|
28
45
|
return { afterPackage: explicit.startsWith('&') ? explicit : `&${explicit}` };
|
|
29
46
|
}
|
|
30
47
|
}
|
|
31
|
-
|
|
48
|
+
// A redirected data plane speaks the backend's parameter order, not the
|
|
49
|
+
// storefront's: the backend decides which flag block it recognises.
|
|
50
|
+
const brand = String(source.brand_override ?? source.brand ?? '').trim();
|
|
32
51
|
if (brand.includes('旋风') || /xuanfeng/i.test(brand)) {
|
|
33
52
|
return { vipFirst: true, viewport: true };
|
|
34
53
|
}
|
|
@@ -47,7 +66,7 @@ export function resolveBrandLayout(source) {
|
|
|
47
66
|
* downloader uses a Chinese product name, so both are mapped. The official
|
|
48
67
|
* host is the last resort when a profile uses a custom source name.
|
|
49
68
|
*/
|
|
50
|
-
const BRAND_APK_PREFIXES = {
|
|
69
|
+
export const BRAND_APK_PREFIXES = {
|
|
51
70
|
雷霆: 'lt',
|
|
52
71
|
旋风: 'xf',
|
|
53
72
|
黑洞: 'hd',
|
|
@@ -490,6 +509,16 @@ function declaredSizeTooLarge(response) {
|
|
|
490
509
|
const length = Number(raw);
|
|
491
510
|
return Number.isFinite(length) && length > MAX_APK_BYTES;
|
|
492
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* Test seam: `defaultLoadApk` with the brand name spelled out.
|
|
514
|
+
*
|
|
515
|
+
* The archive prefix is chosen from this name, and a redirected row must pass
|
|
516
|
+
* its backend's name rather than the row's. Exported so a test can prove which
|
|
517
|
+
* name reaches the loader without standing up a whole extraction.
|
|
518
|
+
*/
|
|
519
|
+
export async function loadApkWithBrand(source, fetchImpl, timeoutMs, now, log, brandName, egress) {
|
|
520
|
+
return defaultLoadApk(source, fetchImpl, timeoutMs, now, log, brandName, egress);
|
|
521
|
+
}
|
|
493
522
|
async function defaultLoadApk(source, fetchImpl, timeoutMs, now, log, brandName = '', egress) {
|
|
494
523
|
if (source.apk_path) {
|
|
495
524
|
try {
|
|
@@ -805,7 +834,9 @@ export async function fetchCredentialSourceLinks(input, options = {}) {
|
|
|
805
834
|
}
|
|
806
835
|
}
|
|
807
836
|
async function fetchCredentialSourceLinksWithTransport(input, options) {
|
|
808
|
-
|
|
837
|
+
// A copy: a redirected data plane rewrites the site and the layout marker,
|
|
838
|
+
// and the caller's config object must not carry that into later stages.
|
|
839
|
+
const source = { ...input.source };
|
|
809
840
|
const platforms = resolveCredentialPlatforms(source);
|
|
810
841
|
const policy = resolveRoundPolicy(source);
|
|
811
842
|
const errors = [];
|
|
@@ -845,6 +876,29 @@ async function fetchCredentialSourceLinksWithTransport(input, options) {
|
|
|
845
876
|
const brand = normalizeBrand(input.source_name)
|
|
846
877
|
?? brandFromSite(source.official_site)
|
|
847
878
|
?? normalizeBrand(credentialBrandKey(input.source_name, source));
|
|
879
|
+
// The account is always leased as the row's own brand; only the data plane
|
|
880
|
+
// can be redirected. See `backend_brand` for why 黑洞 needs this.
|
|
881
|
+
const backendBrand = normalizeBrand(String(source.backend_brand ?? '').trim()) ?? brand;
|
|
882
|
+
if (backendBrand && brand && backendBrand !== brand) {
|
|
883
|
+
const redirected = brandSite(backendBrand);
|
|
884
|
+
if (redirected) {
|
|
885
|
+
// A pinned `apk_path` / `apk_url` is the operator's explicit choice of
|
|
886
|
+
// build and stays as given: it may already be the backend's own APK, and
|
|
887
|
+
// discarding it turned a working pinned profile into "APK could not be
|
|
888
|
+
// located". Only the site moves.
|
|
889
|
+
if (!source.apk_path && !source.apk_url) {
|
|
890
|
+
source.official_site = redirected;
|
|
891
|
+
}
|
|
892
|
+
// The layout is picked from `brand` at every call site; carrying the row
|
|
893
|
+
// name through would keep sending the storefront's own flag block.
|
|
894
|
+
source.brand_override = backendBrand;
|
|
895
|
+
}
|
|
896
|
+
// Informational, not an error: the row is expected to work this way.
|
|
897
|
+
diagnostics.backend_brand = backendBrand;
|
|
898
|
+
event(options, 'log', {
|
|
899
|
+
message: `[extract][${input.source_name}] data plane ${brand} -> ${backendBrand}`
|
|
900
|
+
});
|
|
901
|
+
}
|
|
848
902
|
let store = options.accountStore ?? null;
|
|
849
903
|
let ownsStore = false;
|
|
850
904
|
let leased = null;
|
|
@@ -879,13 +933,25 @@ async function fetchCredentialSourceLinksWithTransport(input, options) {
|
|
|
879
933
|
});
|
|
880
934
|
return { ...empty, failed_iterations: 1 };
|
|
881
935
|
}
|
|
882
|
-
|
|
936
|
+
// Keyed by the data plane, not the storefront: a redirected row downloads and
|
|
937
|
+
// inspects the backend's APK, so it must share that brand's cached entry.
|
|
938
|
+
//
|
|
939
|
+
// A caller-supplied memo cannot be trusted here. The orchestrator builds one
|
|
940
|
+
// per row name before the redirect exists, so `options.memo` for a `heidong`
|
|
941
|
+
// row is seeded from 黑洞's entry while every read and write below goes to the
|
|
942
|
+
// 雷霆 one. That pairing sends the 黑洞 APK's package and seeds to the 雷霆
|
|
943
|
+
// backend on a cached run, and it hides the 雷霆 entry the next run should
|
|
944
|
+
// reuse. A redirect therefore always gets a memo of its own key; the shared
|
|
945
|
+
// one is used only when the two brands agree.
|
|
946
|
+
const memoBrandKey = source.brand_override ?? credentialBrandKey(input.source_name, source);
|
|
947
|
+
const memo = source.brand_override
|
|
948
|
+
? createFingerprintMemo(options.fingerprintStore ?? null, memoBrandKey)
|
|
949
|
+
: (options.memo ?? createFingerprintMemo(options.fingerprintStore ?? null, memoBrandKey));
|
|
883
950
|
let linkQueue = Promise.resolve();
|
|
884
951
|
// A non-numeric retry limit (a hand-edited `account_retry_limit = "three"`)
|
|
885
952
|
// must not turn the attempt loop into zero passes, which reported the source
|
|
886
953
|
// as successful with no links and no error.
|
|
887
|
-
const
|
|
888
|
-
const attempts = explicitAccount && explicitPassword ? 1 : (Number.isFinite(retryLimit) && retryLimit >= 1 ? retryLimit : 3);
|
|
954
|
+
const attempts = explicitAccount && explicitPassword ? 1 : credentialAccountAttempts(source, brand);
|
|
889
955
|
let results = [];
|
|
890
956
|
let callbackFailure = null;
|
|
891
957
|
// The dashboard reads per-source totals from one `extract_iteration` stream.
|
|
@@ -1006,13 +1072,35 @@ async function fetchCredentialSourceLinksWithTransport(input, options) {
|
|
|
1006
1072
|
if (lapsed) {
|
|
1007
1073
|
store.markExpired(leased.id, 'membership expired');
|
|
1008
1074
|
}
|
|
1009
|
-
else if (level !== undefined && level < 2 && !linked) {
|
|
1075
|
+
else if (level !== undefined && level < 2 && !linked && !source.brand_override) {
|
|
1010
1076
|
// A free-tier account (level 0/1) only ever produces placeholders.
|
|
1011
1077
|
// Retire it, but only when it actually produced nothing: a paid
|
|
1012
1078
|
// account misreported as level 1 must not lose its row.
|
|
1079
|
+
//
|
|
1080
|
+
// A redirected row is exempt: the level it reports is the *row*
|
|
1081
|
+
// brand's backend grading it, not the backend it now reads, so a
|
|
1082
|
+
// low level there says nothing about whether the account works.
|
|
1083
|
+
// Retiring on it would discard exactly the accounts the rotation
|
|
1084
|
+
// below is trying to find.
|
|
1013
1085
|
store.markInvalid(leased.id, 'no paid membership');
|
|
1014
1086
|
}
|
|
1015
1087
|
}
|
|
1088
|
+
// A login is not proof the account is usable. On a backend that gates
|
|
1089
|
+
// routes by membership, a free-tier account logs in fine and then sees
|
|
1090
|
+
// every paid row disabled, so the vendor filter leaves nothing to ask.
|
|
1091
|
+
// Publishing that as a finished source was the bug; rotate instead.
|
|
1092
|
+
// The account itself is valid, so it is not retired — only skipped.
|
|
1093
|
+
const producedRoutes = results.some((result) => result.diagnostics.route_count > 0);
|
|
1094
|
+
const producedLinks = results.some((result) => result.links.length > 0);
|
|
1095
|
+
if (!producedRoutes && !producedLinks && store && leased && attempt < attempts && brand) {
|
|
1096
|
+
const next = store.lease(brand);
|
|
1097
|
+
if (next) {
|
|
1098
|
+
leased = next;
|
|
1099
|
+
account = next.account;
|
|
1100
|
+
password = next.password;
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1016
1104
|
break;
|
|
1017
1105
|
}
|
|
1018
1106
|
const membershipExpired = results.length > 0
|
|
@@ -1203,9 +1291,15 @@ async function runCredentialPlatform(input, options) {
|
|
|
1203
1291
|
// These only change when the vendor publishes a new build, so a cached entry
|
|
1204
1292
|
// is used first and the APK is inspected only when the cached seeds all stop
|
|
1205
1293
|
// answering. That keeps a normal run to a handful of HTTP calls.
|
|
1206
|
-
|
|
1294
|
+
// Keyed by the data plane, not the storefront: a redirected row downloads and
|
|
1295
|
+
// inspects the backend's APK, so it must share that brand's cached entry.
|
|
1296
|
+
const brandKey = source.brand_override ?? credentialBrandKey(input.source_name, source);
|
|
1207
1297
|
const store = options.fingerprintStore ?? null;
|
|
1208
|
-
|
|
1298
|
+
// Same rule as the outer memo: a caller-supplied memo is keyed by the row
|
|
1299
|
+
// name, so a redirected row must not read or write through it.
|
|
1300
|
+
const memo = source.brand_override
|
|
1301
|
+
? createFingerprintMemo(store, brandKey)
|
|
1302
|
+
: (options.memo ?? createFingerprintMemo(store, brandKey));
|
|
1209
1303
|
// A fingerprint past its trust window is still used when its seeds answer,
|
|
1210
1304
|
// but it is re-verified against the APK rather than trusted indefinitely: a
|
|
1211
1305
|
// vendor can rotate the signing certificate while the old seeds still serve
|
|
@@ -1228,7 +1322,13 @@ async function runCredentialPlatform(input, options) {
|
|
|
1228
1322
|
try {
|
|
1229
1323
|
apkBytes = options.loadApk
|
|
1230
1324
|
? await options.loadApk(source)
|
|
1231
|
-
: await defaultLoadApk(source, fetchImpl, timeoutMs, now, log,
|
|
1325
|
+
: await defaultLoadApk(source, fetchImpl, timeoutMs, now, log,
|
|
1326
|
+
// The archive prefix names the *backend's* build (`_lt`, not `_hd`):
|
|
1327
|
+
// a redirected row downloads from the backend's CDN, which publishes
|
|
1328
|
+
// only the backend's own archives. Passing the row name here made
|
|
1329
|
+
// every candidate `_hd<N>.apk` against a 雷霆 host, so all lookback
|
|
1330
|
+
// slots missed and the run failed with "APK could not be located".
|
|
1331
|
+
source.brand_override ?? input.source_name, egress);
|
|
1232
1332
|
}
|
|
1233
1333
|
catch (error) {
|
|
1234
1334
|
errors.push(`apk: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1348,7 +1448,7 @@ async function runCredentialPlatform(input, options) {
|
|
|
1348
1448
|
platform,
|
|
1349
1449
|
timestampMs: ts,
|
|
1350
1450
|
code: ''
|
|
1351
|
-
}, resolveBrandLayout({ ...source, brand: input.source_name })), keys, timeoutMs, egress);
|
|
1451
|
+
}, resolveBrandLayout({ ...source, brand: source.brand_override ?? input.source_name })), keys, timeoutMs, egress);
|
|
1352
1452
|
substage('domain_discovery', resolved ? 'success' : 'failed');
|
|
1353
1453
|
return resolved;
|
|
1354
1454
|
}
|
|
@@ -1359,7 +1459,7 @@ async function runCredentialPlatform(input, options) {
|
|
|
1359
1459
|
identity,
|
|
1360
1460
|
device,
|
|
1361
1461
|
platform,
|
|
1362
|
-
layout: resolveBrandLayout({ ...source, brand: input.source_name }),
|
|
1462
|
+
layout: resolveBrandLayout({ ...source, brand: source.brand_override ?? input.source_name }),
|
|
1363
1463
|
token: '',
|
|
1364
1464
|
// Before login the app's local KV has no `code`, so the request carries an
|
|
1365
1465
|
// empty one; `&code=` only holds a value once the account has one.
|