feat: add windows profile model
This commit is contained in:
182
src/server/windowsProfiles.js
Normal file
182
src/server/windowsProfiles.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const ITEM_TYPES = new Set(["process", "folder", "exe"]);
|
||||
const PROTOCOLS = new Set(["TCP", "UDP"]);
|
||||
|
||||
function slug(value, fallback) {
|
||||
const cleaned = String(value || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return cleaned || fallback;
|
||||
}
|
||||
|
||||
function cleanString(value) {
|
||||
return String(value || "").trim();
|
||||
}
|
||||
|
||||
function processName(value) {
|
||||
const base = cleanString(value).split(/[\\/]/).pop() || "";
|
||||
return base.replace(/\.exe$/i, "").trim();
|
||||
}
|
||||
|
||||
function unique(values) {
|
||||
return Array.from(new Set(values.filter(Boolean)));
|
||||
}
|
||||
|
||||
export function normalizeWindowsProfiles(input) {
|
||||
return (Array.isArray(input) ? input : [])
|
||||
.map((profile, index) => {
|
||||
const name = cleanString(profile.name) || `Profile ${index + 1}`;
|
||||
const items = (Array.isArray(profile.items) ? profile.items : [])
|
||||
.filter((item) => ITEM_TYPES.has(item?.type))
|
||||
.map((item) => ({
|
||||
type: item.type,
|
||||
value:
|
||||
item.type === "process"
|
||||
? processName(item.value)
|
||||
: cleanString(item.value),
|
||||
recursive: item.type === "folder" ? item.recursive !== false : false,
|
||||
}))
|
||||
.filter((item) => item.value);
|
||||
|
||||
return {
|
||||
id: slug(profile.id || name, `profile-${index + 1}`),
|
||||
name,
|
||||
enabled: profile.enabled !== false,
|
||||
proxyTargetId: cleanString(profile.proxyTargetId) || "local-singbox",
|
||||
protocols: unique(
|
||||
(Array.isArray(profile.protocols)
|
||||
? profile.protocols
|
||||
: ["TCP", "UDP"])
|
||||
.map((protocol) => cleanString(protocol).toUpperCase())
|
||||
.filter((protocol) => PROTOCOLS.has(protocol)),
|
||||
),
|
||||
items,
|
||||
};
|
||||
})
|
||||
.map((profile) => ({
|
||||
...profile,
|
||||
protocols: profile.protocols.length ? profile.protocols : ["TCP", "UDP"],
|
||||
}));
|
||||
}
|
||||
|
||||
export function normalizeProxyTargets(input) {
|
||||
const local = {
|
||||
id: "local-singbox",
|
||||
name: "Local sing-box",
|
||||
protocol: "socks5",
|
||||
host: "127.0.0.1",
|
||||
port: 1080,
|
||||
managed: true,
|
||||
};
|
||||
const seen = new Set([local.id]);
|
||||
const custom = (Array.isArray(input) ? input : [])
|
||||
.map((target, index) => ({
|
||||
id: slug(target.id || target.name, `target-${index + 1}`),
|
||||
name: cleanString(target.name) || `Proxy target ${index + 1}`,
|
||||
protocol:
|
||||
cleanString(target.protocol || "socks5").toLowerCase() === "http"
|
||||
? "http"
|
||||
: "socks5",
|
||||
host: cleanString(target.host),
|
||||
port: Number.parseInt(target.port, 10),
|
||||
managed: false,
|
||||
}))
|
||||
.filter((target) => {
|
||||
if (!target.host || !Number.isInteger(target.port)) return false;
|
||||
if (target.port <= 0 || target.port > 65535) return false;
|
||||
if (seen.has(target.id)) return false;
|
||||
seen.add(target.id);
|
||||
return true;
|
||||
});
|
||||
return [local, ...custom];
|
||||
}
|
||||
|
||||
function joinPath(base, name, pathSep) {
|
||||
return base.endsWith(pathSep) ? `${base}${name}` : `${base}${pathSep}${name}`;
|
||||
}
|
||||
|
||||
function walkExeFiles(dir, { fsAdapter, recursive, pathSep }) {
|
||||
const entries = fsAdapter.readdirSync(dir, { withFileTypes: true });
|
||||
const results = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = joinPath(dir, entry.name, pathSep);
|
||||
if (entry.isFile() && /\.exe$/i.test(entry.name)) results.push(fullPath);
|
||||
if (recursive && entry.isDirectory()) {
|
||||
results.push(...walkExeFiles(fullPath, { fsAdapter, recursive, pathSep }));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export function resolveProfileItems(items, options = {}) {
|
||||
const fsAdapter = options.fsAdapter || fs;
|
||||
const pathSep = options.pathSep || path.sep;
|
||||
const resolved = [];
|
||||
for (const item of Array.isArray(items) ? items : []) {
|
||||
if (item.type === "process") {
|
||||
const appName = processName(item.value);
|
||||
if (appName) resolved.push({ ...item, appName, source: item.value });
|
||||
}
|
||||
if (item.type === "exe") {
|
||||
const appName = processName(item.value);
|
||||
if (appName) resolved.push({ ...item, appName, source: item.value });
|
||||
}
|
||||
if (item.type === "folder" && fsAdapter.existsSync(item.value)) {
|
||||
const stat = fsAdapter.statSync(item.value);
|
||||
if (stat.isDirectory()) {
|
||||
for (const filePath of walkExeFiles(item.value, {
|
||||
fsAdapter,
|
||||
recursive: item.recursive !== false,
|
||||
pathSep,
|
||||
})) {
|
||||
resolved.push({
|
||||
...item,
|
||||
appName: processName(filePath),
|
||||
source: filePath,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const byName = new Map();
|
||||
for (const item of resolved) {
|
||||
if (!byName.has(item.appName)) byName.set(item.appName, item);
|
||||
}
|
||||
return Array.from(byName.values());
|
||||
}
|
||||
|
||||
export function buildProxiFyreConfig(profiles, targets, options = {}) {
|
||||
const normalizedTargets = normalizeProxyTargets(targets);
|
||||
const targetById = new Map(
|
||||
normalizedTargets.map((target) => [target.id, target]),
|
||||
);
|
||||
const groups = new Map();
|
||||
for (const profile of normalizeWindowsProfiles(profiles).filter(
|
||||
(item) => item.enabled,
|
||||
)) {
|
||||
const target =
|
||||
targetById.get(profile.proxyTargetId) || targetById.get("local-singbox");
|
||||
const resolved = resolveProfileItems(profile.items, options);
|
||||
if (!target || resolved.length === 0) continue;
|
||||
const key = `${target.id}|${profile.protocols.join(",")}`;
|
||||
const existing = groups.get(key) || {
|
||||
appNames: [],
|
||||
socks5ProxyEndpoint: `${target.host}:${target.port}`,
|
||||
supportedProtocols: profile.protocols,
|
||||
};
|
||||
existing.appNames.push(...resolved.map((item) => item.appName));
|
||||
existing.appNames = unique(existing.appNames).sort((a, b) =>
|
||||
a.localeCompare(b),
|
||||
);
|
||||
groups.set(key, existing);
|
||||
}
|
||||
return {
|
||||
logLevel: "Info",
|
||||
proxies: Array.from(groups.values()),
|
||||
excludes: [],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user