Remove initial server health check and bump Harbor versions
This commit is contained in:
+34
-12
@@ -43,6 +43,11 @@ import { buildGatewayVersionInfo, buildVersionInfo } from './version.js';
|
||||
const MAX_BODY_BYTES = 1_000_000;
|
||||
const SUBSCRIPTION_REFRESH_INTERVAL_MS = 15 * 60 * 1000;
|
||||
const GATEWAY_DISCOVERY_INTERVAL_MS = 5_000;
|
||||
const TERMINAL_SUBSCRIPTION_CODES = new Set([
|
||||
'SUBSCRIPTION_EXPIRED',
|
||||
'SUBSCRIPTION_DISABLED',
|
||||
'SUBSCRIPTION_REJECTED',
|
||||
]);
|
||||
|
||||
fs.mkdirSync(settings.dataDir, { recursive: true });
|
||||
|
||||
@@ -215,6 +220,16 @@ function buildActiveConfig(
|
||||
const stopSingbox = () => singboxRuntime.stop();
|
||||
const startSingbox = () => singboxRuntime.apply();
|
||||
|
||||
function resetSavedSubscription({ stopRuntime = true } = {}) {
|
||||
return serializeControl(async () => {
|
||||
if (stopRuntime) await stopSingbox();
|
||||
removeSingboxConfig();
|
||||
subscriptionCacheStore.remove();
|
||||
updateStoredState((state) => ({ routeRules: state.routeRules }));
|
||||
gatewayAutoState = createGatewayAutoState();
|
||||
});
|
||||
}
|
||||
|
||||
async function publicState() {
|
||||
const runtime = await singboxRuntime.refresh();
|
||||
const state = normalizeStoredState(stateStore.read());
|
||||
@@ -529,10 +544,17 @@ function refreshSavedSubscription() {
|
||||
if (subscriptionRefreshPromise) return subscriptionRefreshPromise;
|
||||
|
||||
subscriptionRefreshPromise = (async () => {
|
||||
const subscriptionUrl = stateStore.read().subscriptionUrl;
|
||||
if (!subscriptionUrl) throw new HarborError('SUBSCRIPTION_INVALID');
|
||||
const parsed = await fetchSubscription(subscriptionUrl);
|
||||
return commitSubscription(subscriptionUrl, parsed);
|
||||
try {
|
||||
const subscriptionUrl = stateStore.read().subscriptionUrl;
|
||||
if (!subscriptionUrl) throw new HarborError('SUBSCRIPTION_INVALID');
|
||||
const parsed = await fetchSubscription(subscriptionUrl);
|
||||
return await commitSubscription(subscriptionUrl, parsed);
|
||||
} catch (error) {
|
||||
if (TERMINAL_SUBSCRIPTION_CODES.has(error?.code)) {
|
||||
await resetSavedSubscription();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
})().finally(() => {
|
||||
subscriptionRefreshPromise = null;
|
||||
});
|
||||
@@ -654,13 +676,7 @@ async function handleApi(req, res) {
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE' && req.url === '/api/subscription') {
|
||||
await withOperation('subscription-forget', () => serializeControl(async () => {
|
||||
await stopSingbox();
|
||||
removeSingboxConfig();
|
||||
subscriptionCacheStore.remove();
|
||||
updateStoredState((state) => ({ routeRules: state.routeRules }));
|
||||
gatewayAutoState = createGatewayAutoState();
|
||||
}));
|
||||
await withOperation('subscription-forget', () => resetSavedSubscription());
|
||||
return sendState(res);
|
||||
}
|
||||
|
||||
@@ -755,7 +771,13 @@ process.on('SIGINT', shutdown);
|
||||
await refreshGatewayAutoMode({ reconfigure: false })
|
||||
.catch((error) => console.warn(`[control] Gateway не определён: ${error.message}`));
|
||||
if (settings.appMode === 'client' || !fs.existsSync(settings.configPath)) {
|
||||
writeCurrentConfig();
|
||||
try {
|
||||
writeCurrentConfig();
|
||||
} catch (error) {
|
||||
if (!String(error?.code || '').startsWith('SUBSCRIPTION_')) throw error;
|
||||
console.warn(`[storage] сохранённая подписка отклонена: ${error.message}; возврат к первичной настройке`);
|
||||
await resetSavedSubscription({ stopRuntime: false });
|
||||
}
|
||||
}
|
||||
await startSingbox()
|
||||
.then(() => {
|
||||
|
||||
@@ -10,6 +10,23 @@ import {
|
||||
import { atomicWriteFile } from './services/stateStore.js';
|
||||
|
||||
const PROXY_TYPES = new Set(['vless', 'vmess', 'trojan', 'shadowsocks', 'hysteria2']);
|
||||
const UNSPECIFIED_HOSTS = new Set(['0.0.0.0', '::', '[::]']);
|
||||
|
||||
function usableProxyOutbound(outbound) {
|
||||
const host = String(outbound?.server || '').trim().toLowerCase();
|
||||
const port = Number(outbound?.server_port);
|
||||
return Boolean(host) && !UNSPECIFIED_HOSTS.has(host) && Number.isInteger(port) && port > 0 && port <= 65535;
|
||||
}
|
||||
|
||||
function rejectedSubscriptionCode(outbounds) {
|
||||
const labels = outbounds.map((outbound) => String(outbound?.tag || '').toLowerCase()).join(' ');
|
||||
if (labels.includes('expired')) return 'SUBSCRIPTION_EXPIRED';
|
||||
if (labels.includes('disabled')) return 'SUBSCRIPTION_DISABLED';
|
||||
if (/traffic|quota|bandwidth|трафик/.test(labels)) return 'SUBSCRIPTION_TRAFFIC_EXHAUSTED';
|
||||
return outbounds.some((outbound) => UNSPECIFIED_HOSTS.has(String(outbound?.server || '').trim().toLowerCase()))
|
||||
? 'SUBSCRIPTION_REJECTED'
|
||||
: 'SUBSCRIPTION_INVALID';
|
||||
}
|
||||
|
||||
export function getHwid() {
|
||||
fs.mkdirSync(settings.dataDir, { recursive: true });
|
||||
@@ -114,9 +131,18 @@ export function normalizeSubscriptionConfig(value) {
|
||||
const parsedConfig = value && typeof value === 'object' ? value : {};
|
||||
const outbounds = Array.isArray(parsedConfig.outbounds) ? parsedConfig.outbounds : [];
|
||||
const servers = [];
|
||||
const rejectedOutbounds = [];
|
||||
const seen = new Set();
|
||||
const normalizedOutbounds = outbounds.flatMap((outbound) => {
|
||||
if (!outbound || typeof outbound !== 'object') {
|
||||
rejectedOutbounds.push(outbound);
|
||||
return [];
|
||||
}
|
||||
if (!PROXY_TYPES.has(outbound.type)) return [outbound];
|
||||
if (!usableProxyOutbound(outbound)) {
|
||||
rejectedOutbounds.push(outbound);
|
||||
return [];
|
||||
}
|
||||
const id = createServerId(outbound);
|
||||
// ponytail: endpoint identity deduplicates indistinguishable entries; include provider IDs if real feeds need same-endpoint variants.
|
||||
if (seen.has(id)) return [];
|
||||
@@ -125,7 +151,7 @@ export function normalizeSubscriptionConfig(value) {
|
||||
return [{ ...outbound, tag: id }];
|
||||
});
|
||||
|
||||
if (!servers.length) throw new HarborError('SUBSCRIPTION_INVALID');
|
||||
if (!servers.length) throw new HarborError(rejectedSubscriptionCode(rejectedOutbounds));
|
||||
return { config: { ...parsedConfig, outbounds: normalizedOutbounds }, servers };
|
||||
}
|
||||
|
||||
@@ -198,6 +224,12 @@ export async function fetchSubscription(url, options) {
|
||||
|
||||
const body = await response.text();
|
||||
const userInfo = parseUserInfo(response.headers.get('subscription-userinfo'));
|
||||
if (userInfo.expire > 0 && userInfo.expire * 1000 <= Date.now()) {
|
||||
throw new HarborError('SUBSCRIPTION_EXPIRED');
|
||||
}
|
||||
if (userInfo.total > 0 && (userInfo.upload || 0) + (userInfo.download || 0) >= userInfo.total) {
|
||||
throw new HarborError('SUBSCRIPTION_TRAFFIC_EXHAUSTED');
|
||||
}
|
||||
const parsed = parseSubscriptionBody(body);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user