211 lines
6.5 KiB
JavaScript
211 lines
6.5 KiB
JavaScript
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: [],
|
|
};
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|