Refactor VPN proxy components and update related behavior
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user