Refactor VPN proxy components and update related behavior
Build and Deploy Gateway / build-and-push (push) Successful in 20s
Build and Deploy Gateway / deploy (push) Successful in 13s

This commit is contained in:
2026-08-11 01:27:46 +03:00
parent c89e56942a
commit aa9c959368
58 changed files with 4234 additions and 2755 deletions
+19 -4
View File
@@ -5,18 +5,33 @@ import type { ConnectionService } from '../../features/connection/index.js';
interface ServerApplyRouteDependencies {
connection: Pick<ConnectionService, 'apply'>;
readBody(req: IncomingMessage): Promise<Record<string, unknown>>;
withOperation<T>(kind: string, operation: () => Promise<T>): Promise<T>;
sendState(res: ServerResponse, extra: { serverId: string; selectedTag: string }): Promise<void>;
withOperation<T>(
kind: string,
operation: (operationRevision: number) => Promise<T>,
options?: { expectedRevision?: unknown; profileId?: unknown; serverId?: unknown },
): Promise<T>;
sendState(res: ServerResponse, extra: { profileId: string; serverId: string; selectedTag: string }): Promise<void>;
}
export function createServerApplyRoute(dependencies: ServerApplyRouteDependencies) {
return {
async handle(req: IncomingMessage, res: ServerResponse) {
if (req.method !== 'POST' || req.url !== '/api/apply') return false;
const { serverId = '', selectedTag = '' } = await dependencies.readBody(req);
const {
profileId = '',
serverId = '',
selectedTag = '',
expectedRevision,
} = await dependencies.readBody(req);
const result = await dependencies.withOperation(
'apply-server',
() => dependencies.connection.apply(serverId, selectedTag),
(operationRevision) => dependencies.connection.apply(
profileId,
serverId,
selectedTag,
operationRevision,
),
{ expectedRevision, profileId, serverId },
);
await dependencies.sendState(res, result);
return true;
+10 -4
View File
@@ -5,16 +5,22 @@ import type { ServerHealthService } from '../../features/servers/index.js';
interface ServerHealthRouteDependencies {
serverHealth: ServerHealthService;
readBody(req: IncomingMessage): Promise<Record<string, unknown>>;
sendState(res: ServerResponse, extra: { results: Array<Record<string, unknown>> }): Promise<void>;
sendState(res: ServerResponse, extra: {
profileId: string;
results: Array<Record<string, unknown>>;
}): Promise<void>;
}
export function createServerHealthRoute(dependencies: ServerHealthRouteDependencies) {
return {
async handle(req: IncomingMessage, res: ServerResponse) {
if (req.method !== 'POST' || req.url !== '/api/servers/ping-all') return false;
const pathname = new URL(req.url || '/', 'http://localhost').pathname;
const profileMatch = pathname.match(/^\/api\/profiles\/([^/]+)\/servers\/ping$/);
if (req.method !== 'POST' || (!profileMatch && pathname !== '/api/servers/ping-all')) return false;
const { serverIds = [] } = await dependencies.readBody(req);
const results = await dependencies.serverHealth.check(serverIds);
await dependencies.sendState(res, { results });
const profileId = profileMatch ? decodeURIComponent(profileMatch[1]) : '';
const results = await dependencies.serverHealth.check(profileId, serverIds);
await dependencies.sendState(res, { profileId, results });
return true;
},
};
+1 -1
View File
@@ -46,7 +46,7 @@ function withStateV0Compatibility(
singboxRunning: snapshot.connection.process === 'running',
singboxStartedAt: snapshot.connection.startedAt,
subscriptionHost: snapshot.subscription.host,
hasSubscription: snapshot.subscription.status === 'ready',
hasSubscription: snapshot.subscription.status !== 'missing',
selectedTag: stored.selectedTag,
userInfo: snapshot.subscription.userInfo,
fetchedAt: snapshot.subscription.fetchedAt,
@@ -1,43 +1,170 @@
import type { IncomingMessage, ServerResponse } from 'node:http';
import type { ConnectionService } from '../../features/connection/index.js';
import type { SubscriptionService } from '../../features/subscription/index.js';
interface OperationOptions {
expectedRevision?: unknown;
profileId?: unknown;
serverId?: unknown;
}
interface SubscriptionMutationRouteDependencies {
subscriptionService: Pick<
SubscriptionService,
'importSubscription' | 'refreshSavedSubscription' | 'resetSavedSubscription'
| 'preflightAddProfile'
| 'preflightRenameProfile'
| 'addProfile'
| 'renameProfile'
| 'selectProfileServer'
| 'refreshProfile'
| 'deleteProfile'
| 'importSubscription'
| 'refreshSavedSubscription'
| 'resetSavedSubscription'
>;
connection: Pick<ConnectionService, 'activate'>;
readBody(req: IncomingMessage): Promise<Record<string, unknown>>;
withOperation<T>(kind: string, operation: () => Promise<T>): Promise<T>;
withOperation<T>(
kind: string,
operation: (operationRevision: number) => Promise<T>,
options?: OperationOptions,
): Promise<T>;
sendState(res: ServerResponse, extra?: Record<string, unknown>): Promise<void>;
}
export function createSubscriptionMutationRoute(dependencies: SubscriptionMutationRouteDependencies) {
return {
async handle(req: IncomingMessage, res: ServerResponse) {
if (req.method === 'POST' && req.url === '/api/subscription/fetch') {
const pathname = new URL(req.url || '/', 'http://localhost').pathname;
const profileMatch = pathname.match(/^\/api\/profiles\/([^/]+)$/);
const serverMatch = pathname.match(/^\/api\/profiles\/([^/]+)\/server$/);
const activateMatch = pathname.match(/^\/api\/profiles\/([^/]+)\/activate$/);
const refreshMatch = pathname.match(/^\/api\/profiles\/([^/]+)\/refresh$/);
if (req.method === 'POST' && pathname === '/api/profiles') {
const { label = '', url = '', expectedRevision } = await dependencies.readBody(req);
dependencies.subscriptionService.preflightAddProfile(label, expectedRevision);
const result = await dependencies.withOperation(
'profile-add',
(operationRevision) => dependencies.subscriptionService.addProfile(
label,
url,
operationRevision,
),
{ expectedRevision },
);
await dependencies.sendState(res, result);
return true;
}
if (req.method === 'PATCH' && profileMatch) {
const profileId = decodeURIComponent(profileMatch[1]);
const { label = '', expectedRevision } = await dependencies.readBody(req);
dependencies.subscriptionService.preflightRenameProfile(profileId, label, expectedRevision);
const result = await dependencies.withOperation(
'profile-rename',
(operationRevision) => dependencies.subscriptionService.renameProfile(
profileId,
label,
operationRevision,
),
{ expectedRevision, profileId },
);
await dependencies.sendState(res, result);
return true;
}
if (req.method === 'PUT' && serverMatch) {
const profileId = decodeURIComponent(serverMatch[1]);
const { serverId = '', expectedRevision } = await dependencies.readBody(req);
const result = await dependencies.withOperation(
'profile-select-server',
(operationRevision) => dependencies.subscriptionService.selectProfileServer(
profileId,
serverId,
operationRevision,
),
{ expectedRevision, profileId, serverId },
);
await dependencies.sendState(res, result);
return true;
}
if (req.method === 'POST' && activateMatch) {
const profileId = decodeURIComponent(activateMatch[1]);
const { expectedRevision } = await dependencies.readBody(req);
const result = await dependencies.withOperation(
'profile-activate',
(operationRevision) => dependencies.connection.activate(profileId, operationRevision),
{ expectedRevision, profileId },
);
await dependencies.sendState(res, result);
return true;
}
if (req.method === 'POST' && refreshMatch) {
const profileId = decodeURIComponent(refreshMatch[1]);
const { expectedRevision } = await dependencies.readBody(req);
const result = await dependencies.withOperation(
'profile-refresh',
(operationRevision) => dependencies.subscriptionService.refreshProfile(
profileId,
operationRevision,
),
{ expectedRevision, profileId },
);
await dependencies.sendState(res, result);
return true;
}
if (req.method === 'DELETE' && profileMatch) {
const profileId = decodeURIComponent(profileMatch[1]);
const { mode = 'delete', expectedRevision } = await dependencies.readBody(req);
const result = await dependencies.withOperation(
'profile-delete',
(operationRevision) => dependencies.subscriptionService.deleteProfile(
profileId,
mode,
operationRevision,
),
{ expectedRevision, profileId },
);
await dependencies.sendState(res, result);
return true;
}
// One-release compatibility for the old single-subscription client.
if (req.method === 'POST' && pathname === '/api/subscription/fetch') {
const { url = '' } = await dependencies.readBody(req);
const result = await dependencies.withOperation(
'subscription-import',
() => dependencies.subscriptionService.importSubscription(String(url).trim()),
(operationRevision) => dependencies.subscriptionService.importSubscription(
String(url).trim(),
operationRevision,
),
);
await dependencies.sendState(res, result);
return true;
}
if (req.method === 'POST' && req.url === '/api/subscription/refresh') {
const { success: _success, ...result } = await dependencies.withOperation(
if (req.method === 'POST' && pathname === '/api/subscription/refresh') {
const result = await dependencies.withOperation(
'subscription-refresh',
() => dependencies.subscriptionService.refreshSavedSubscription(),
(operationRevision) => dependencies.subscriptionService.refreshSavedSubscription(
operationRevision,
),
);
await dependencies.sendState(res, result);
return true;
}
if (req.method === 'DELETE' && req.url === '/api/subscription') {
if (req.method === 'DELETE' && pathname === '/api/subscription') {
await dependencies.withOperation(
'subscription-forget',
() => dependencies.subscriptionService.resetSavedSubscription(),
(operationRevision) => dependencies.subscriptionService.resetSavedSubscription({
expectedRevision: operationRevision,
}),
);
await dependencies.sendState(res);
return true;