feat: add windows API model
This commit is contained in:
@@ -27,6 +27,14 @@ import {
|
|||||||
} from "./sharedProxy.js";
|
} from "./sharedProxy.js";
|
||||||
import { matchRoute, detectRuleConflicts } from "./routeMatcher.js";
|
import { matchRoute, detectRuleConflicts } from "./routeMatcher.js";
|
||||||
import { tcpPing, resolveHost } from "./ping.js";
|
import { tcpPing, resolveHost } from "./ping.js";
|
||||||
|
import {
|
||||||
|
buildProxiFyreConfig,
|
||||||
|
createActivityEntry,
|
||||||
|
normalizeProxyTargets,
|
||||||
|
normalizeWindowsProfiles,
|
||||||
|
summarizeProfiles,
|
||||||
|
} from "./windowsProfiles.js";
|
||||||
|
import { windowsHelper } from "./windowsHelper.js";
|
||||||
|
|
||||||
const APPLY_HISTORY_LIMIT = 10;
|
const APPLY_HISTORY_LIMIT = 10;
|
||||||
const RULE_SET_TAG_RE = /^[a-z0-9][a-z0-9_.@!-]*$/i;
|
const RULE_SET_TAG_RE = /^[a-z0-9][a-z0-9_.@!-]*$/i;
|
||||||
@@ -603,6 +611,8 @@ function publicState() {
|
|||||||
const customRules = readJson(settings.customRulesPath, []);
|
const customRules = readJson(settings.customRulesPath, []);
|
||||||
const deviceProfiles = readDeviceProfiles();
|
const deviceProfiles = readDeviceProfiles();
|
||||||
const clientSettings = readClientSettings();
|
const clientSettings = readClientSettings();
|
||||||
|
const windowsTargets =
|
||||||
|
settings.appMode === "windows" ? readProxyTargets() : [];
|
||||||
const { subscriptionUrl, ...rest } = state;
|
const { subscriptionUrl, ...rest } = state;
|
||||||
return {
|
return {
|
||||||
mode: settings.appMode,
|
mode: settings.appMode,
|
||||||
@@ -634,6 +644,17 @@ function publicState() {
|
|||||||
directBypassCount,
|
directBypassCount,
|
||||||
directBypassEnabled: DIRECT_BYPASS_CACHE,
|
directBypassEnabled: DIRECT_BYPASS_CACHE,
|
||||||
directBypassAvailable: IPSET_AVAILABLE,
|
directBypassAvailable: IPSET_AVAILABLE,
|
||||||
|
windows:
|
||||||
|
settings.appMode === "windows"
|
||||||
|
? {
|
||||||
|
profiles: summarizeProfiles(
|
||||||
|
readWindowsProfiles(),
|
||||||
|
windowsTargets,
|
||||||
|
),
|
||||||
|
targets: windowsTargets,
|
||||||
|
activity: readWindowsActivity().slice(-20).reverse(),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
...rest,
|
...rest,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -686,6 +707,62 @@ function normalizeDeviceRules(input) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readWindowsProfiles() {
|
||||||
|
return normalizeWindowsProfiles(readJson(settings.windowsProfilesPath, []));
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeWindowsProfiles(profiles) {
|
||||||
|
const normalized = normalizeWindowsProfiles(profiles);
|
||||||
|
writeJson(settings.windowsProfilesPath, normalized);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readProxyTargets() {
|
||||||
|
return normalizeProxyTargets(readJson(settings.windowsTargetsPath, []));
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeProxyTargets(targets) {
|
||||||
|
const normalized = normalizeProxyTargets(targets);
|
||||||
|
writeJson(
|
||||||
|
settings.windowsTargetsPath,
|
||||||
|
normalized.filter((target) => !target.managed),
|
||||||
|
);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readWindowsActivity() {
|
||||||
|
return readJson(settings.windowsActivityPath, []).slice(-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushWindowsActivity(type, message, details = {}) {
|
||||||
|
const activity = readWindowsActivity();
|
||||||
|
const entry = createActivityEntry(type, message, details);
|
||||||
|
writeJson(settings.windowsActivityPath, [...activity, entry].slice(-100));
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWindowsStatus() {
|
||||||
|
let helperStatus = null;
|
||||||
|
if (settings.appMode === "windows") {
|
||||||
|
try {
|
||||||
|
helperStatus = await windowsHelper.run("status.get", {});
|
||||||
|
} catch (error) {
|
||||||
|
helperStatus = { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const profiles = readWindowsProfiles();
|
||||||
|
const targets = readProxyTargets();
|
||||||
|
return {
|
||||||
|
mode: settings.appMode,
|
||||||
|
installMode:
|
||||||
|
readJson(settings.windowsStatePath, {}).installMode || "not-configured",
|
||||||
|
profiles: summarizeProfiles(profiles, targets),
|
||||||
|
targets,
|
||||||
|
activity: readWindowsActivity().slice(-20).reverse(),
|
||||||
|
helperStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function applySelectedServer(selectedTag) {
|
async function applySelectedServer(selectedTag) {
|
||||||
const cached = readJson(settings.subscriptionCachePath, null);
|
const cached = readJson(settings.subscriptionCachePath, null);
|
||||||
if (!cached?.config) {
|
if (!cached?.config) {
|
||||||
@@ -809,6 +886,99 @@ async function handleApi(req, res) {
|
|||||||
return sendJson(res, 200, { success: true, config });
|
return sendJson(res, 200, { success: true, config });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && req.url === "/api/windows/status") {
|
||||||
|
return sendJson(res, 200, { success: true, ...(await getWindowsStatus()) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && req.url === "/api/windows/profiles") {
|
||||||
|
const profiles = readWindowsProfiles();
|
||||||
|
const targets = readProxyTargets();
|
||||||
|
return sendJson(res, 200, {
|
||||||
|
success: true,
|
||||||
|
profiles,
|
||||||
|
summaries: summarizeProfiles(profiles, targets),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "PUT" && req.url === "/api/windows/profiles") {
|
||||||
|
const body = await readBody(req);
|
||||||
|
const profiles = writeWindowsProfiles(body.profiles || []);
|
||||||
|
pushWindowsActivity("profiles.saved", "Profiles saved", {
|
||||||
|
count: profiles.length,
|
||||||
|
});
|
||||||
|
return sendJson(res, 200, {
|
||||||
|
success: true,
|
||||||
|
profiles,
|
||||||
|
summaries: summarizeProfiles(profiles, readProxyTargets()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && req.url === "/api/windows/profiles/scan") {
|
||||||
|
const body = await readBody(req);
|
||||||
|
const profiles = normalizeWindowsProfiles(body.profiles || []);
|
||||||
|
return sendJson(res, 200, {
|
||||||
|
success: true,
|
||||||
|
summaries: summarizeProfiles(profiles, readProxyTargets()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && req.url === "/api/windows/profiles/apply") {
|
||||||
|
const profiles = readWindowsProfiles();
|
||||||
|
const targets = readProxyTargets();
|
||||||
|
const proxifyreConfig = buildProxiFyreConfig(profiles, targets);
|
||||||
|
const helperResult = await windowsHelper.run("proxifyre.apply", {
|
||||||
|
configPath: settings.proxifyreConfigPath,
|
||||||
|
config: proxifyreConfig,
|
||||||
|
});
|
||||||
|
pushWindowsActivity("profiles.applied", "ProxiFyre config applied", {
|
||||||
|
proxyGroups: proxifyreConfig.proxies.length,
|
||||||
|
});
|
||||||
|
return sendJson(res, 200, {
|
||||||
|
success: true,
|
||||||
|
config: proxifyreConfig,
|
||||||
|
helperResult,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && req.url === "/api/windows/targets") {
|
||||||
|
return sendJson(res, 200, { success: true, targets: readProxyTargets() });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "PUT" && req.url === "/api/windows/targets") {
|
||||||
|
const body = await readBody(req);
|
||||||
|
const targets = writeProxyTargets(body.targets || []);
|
||||||
|
pushWindowsActivity("targets.saved", "Proxy targets saved", {
|
||||||
|
count: targets.length,
|
||||||
|
});
|
||||||
|
return sendJson(res, 200, { success: true, targets });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "POST" && req.url === "/api/windows/service") {
|
||||||
|
const body = await readBody(req);
|
||||||
|
const service = String(body.service || "");
|
||||||
|
const action = String(body.action || "");
|
||||||
|
if (!["sing-box", "proxifyre", "ui"].includes(service)) {
|
||||||
|
return sendJson(res, 400, { success: false, error: "Unknown service" });
|
||||||
|
}
|
||||||
|
if (!["start", "stop", "restart"].includes(action)) {
|
||||||
|
return sendJson(res, 400, { success: false, error: "Unknown action" });
|
||||||
|
}
|
||||||
|
const helperResult = await windowsHelper.run("service.control", {
|
||||||
|
service,
|
||||||
|
action,
|
||||||
|
});
|
||||||
|
pushWindowsActivity("service.control", `${service} ${action}`, {
|
||||||
|
service,
|
||||||
|
action,
|
||||||
|
});
|
||||||
|
return sendJson(res, 200, { success: true, helperResult });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === "GET" && req.url === "/api/windows/logs") {
|
||||||
|
const helperResult = await windowsHelper.run("logs.get", {});
|
||||||
|
return sendJson(res, 200, { success: true, helperResult });
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method === "GET" && req.url === "/api/logs") {
|
if (req.method === "GET" && req.url === "/api/logs") {
|
||||||
return sendJson(res, 200, { success: true, logs: logBuffer.slice(-200) });
|
return sendJson(res, 200, { success: true, logs: logBuffer.slice(-200) });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,3 +180,31 @@ export function buildProxiFyreConfig(profiles, targets, options = {}) {
|
|||||||
excludes: [],
|
excludes: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function summarizeProfiles(profiles, targets, options = {}) {
|
||||||
|
const normalizedTargets = normalizeProxyTargets(targets);
|
||||||
|
const targetById = new Map(
|
||||||
|
normalizedTargets.map((target) => [target.id, target]),
|
||||||
|
);
|
||||||
|
return normalizeWindowsProfiles(profiles).map((profile) => {
|
||||||
|
const resolvedItems = resolveProfileItems(profile.items, options);
|
||||||
|
const target =
|
||||||
|
targetById.get(profile.proxyTargetId) || targetById.get("local-singbox");
|
||||||
|
return {
|
||||||
|
...profile,
|
||||||
|
target,
|
||||||
|
resolvedCount: resolvedItems.length,
|
||||||
|
resolvedItems,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createActivityEntry(type, message, details = {}) {
|
||||||
|
return {
|
||||||
|
id: `${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
details,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
26
test/server/windows-api-model.test.js
Normal file
26
test/server/windows-api-model.test.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import {
|
||||||
|
buildProxiFyreConfig,
|
||||||
|
normalizeProxyTargets,
|
||||||
|
normalizeWindowsProfiles,
|
||||||
|
summarizeProfiles,
|
||||||
|
} from "../../src/server/windowsProfiles.js";
|
||||||
|
|
||||||
|
test("windows API model returns summaries and generated config", () => {
|
||||||
|
const profiles = normalizeWindowsProfiles([
|
||||||
|
{
|
||||||
|
name: "Discord",
|
||||||
|
proxyTargetId: "local-singbox",
|
||||||
|
items: [{ type: "process", value: "Discord" }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const targets = normalizeProxyTargets([]);
|
||||||
|
|
||||||
|
const summaries = summarizeProfiles(profiles, targets);
|
||||||
|
const config = buildProxiFyreConfig(profiles, targets);
|
||||||
|
|
||||||
|
assert.equal(summaries[0].resolvedCount, 1);
|
||||||
|
assert.equal(summaries[0].target.id, "local-singbox");
|
||||||
|
assert.deepEqual(config.proxies[0].appNames, ["Discord"]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user