Files
NeoTachyon/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js
T

12739 lines
353 KiB
JavaScript

"use strict";
"require form";
"require baseclass";
"require fs";
"require network";
"require ui";
"require uci";
"require view.tachyon.local_devices as localDevices";
"require view.tachyon.main as main";
const UCI_PACKAGE = main.TACHYON_UCI_PACKAGE;
const ACTION_PROVIDERS_AVAILABILITY_EVENT =
main.TACHYON_ACTION_PROVIDERS_AVAILABILITY_EVENT ||
"tachyon:action-providers-availability";
const RULE_SET_ITEM_SETTINGS_KEY = "rule_set_settings";
const ROUTING_ACTIONS = [
"connection",
"proxy",
"outbound",
"vpn",
"bypass",
"block",
"zapret",
"zapret2",
"byedpi",
"wdtt",
"olcrtc",
"awg",
"warp",
"anytls",
"snell",
"mieru",
"sudoku",
"masque",
"openvpn",
"hosts",
];
const CONNECTIONS_BLOCKED_INTERFACES = [
"br-lan",
"eth0",
"eth1",
"wan",
"phy0-ap0",
"phy1-ap0",
"pppoe-wan",
"lan",
];
function valuesToText(values) {
if (!values) {
return "";
}
if (Array.isArray(values)) {
return values.filter(Boolean).join("\n");
}
return values ? `${values}` : "";
}
function normalizeOptionValues(value) {
if (!value) {
return [];
}
if (Array.isArray(value)) {
return value
.filter(Boolean)
.map((item) => `${item}`.trim())
.filter(Boolean);
}
return `${value}`
.split(/\s+/)
.map((item) => item.trim())
.filter(Boolean);
}
function getUciSectionName(section) {
return section && section[".name"] ? section[".name"] : "";
}
function getUciSectionLabel(section) {
return (section && section.label) || getUciSectionName(section);
}
function isOutboundDetourTargetSection(section, currentSectionId) {
const sectionName = getUciSectionName(section);
const action = (section && section.action) || "";
return (
sectionName &&
sectionName !== currentSectionId &&
section.enabled !== "0" &&
[
"connection",
"proxy",
"outbound",
"vpn",
"awg",
"warp",
"anytls",
"snell",
"mieru",
"sudoku",
"masque",
"openvpn",
].includes(action)
);
}
function getOutboundDetourTargetSections(currentSectionId) {
return (uci.sections(UCI_PACKAGE, "section") || []).filter((section) =>
isOutboundDetourTargetSection(section, currentSectionId),
);
}
function getDefaultOutboundDetourSection(currentSectionId) {
const targetSections = getOutboundDetourTargetSections(currentSectionId);
return targetSections.length ? getUciSectionName(targetSections[0]) : "";
}
function refreshOutboundDetourSectionOptionValues(option, sectionId) {
option.keylist = [];
option.vallist = [];
getOutboundDetourTargetSections(sectionId).forEach((targetSection) => {
option.value(
getUciSectionName(targetSection),
getUciSectionLabel(targetSection),
);
});
}
function isDnsDetourTargetSection(section, currentSectionId) {
const sectionName = getUciSectionName(section);
const action = (section && section.action) || "";
if (
!sectionName ||
sectionName === currentSectionId ||
section.enabled === "0"
) {
return false;
}
if (
[
"connection",
"proxy",
"outbound",
"vpn",
"awg",
"warp",
"anytls",
"snell",
"mieru",
"sudoku",
"masque",
"openvpn",
].includes(action)
) {
return true;
}
if (action === "zapret") {
return isZapretInstalledForUi();
}
if (action === "zapret2") {
return isZapret2InstalledForUi();
}
return action === "byedpi" && isByedpiInstalledForUi();
}
function refreshDnsDetourSectionOptionValues(option, sectionId) {
option.keylist = [];
option.vallist = [];
(uci.sections(UCI_PACKAGE, "section") || [])
.filter((section) => isDnsDetourTargetSection(section, sectionId))
.forEach((targetSection) => {
option.value(
getUciSectionName(targetSection),
getUciSectionLabel(targetSection),
);
});
}
function dependsOnRoutingAction(option) {
ROUTING_ACTIONS.forEach((action) => option.depends("action", action));
return option;
}
function dependsOnRuleConditions(option) {
const routingConditions = [
"domain",
"ip_cidr",
"community_lists",
"rule_set",
"domain_ip_lists",
"ports",
];
ROUTING_ACTIONS.forEach((action) =>
routingConditions.forEach((condition) =>
option.depends({ action, [condition]: /\S/ }),
),
);
[
"domain",
"community_lists",
"_dns_rule_set",
"_dns_domain_ip_lists",
].forEach((condition) =>
option.depends({ action: "dns", [condition]: /\S/ }),
);
return option;
}
const ZAPRET_LEGACY_DEFAULT_NFQWS_OPT =
"--filter-tcp=80 <HOSTLIST> --dpi-desync=fake,fakedsplit --dpi-desync-autottl=2 --dpi-desync-fooling=badsum --new --filter-tcp=443 --hostlist=/opt/zapret/ipset/zapret-hosts-google.txt --dpi-desync=fake,multidisorder --dpi-desync-split-pos=1,midsld --dpi-desync-repeats=11 --dpi-desync-fooling=badsum --dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com --new --filter-udp=443 --hostlist=/opt/zapret/ipset/zapret-hosts-google.txt --dpi-desync=fake --dpi-desync-repeats=11 --dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin --new --filter-udp=443 <HOSTLIST_NOAUTO> --dpi-desync=fake --dpi-desync-repeats=11 --new --filter-tcp=443 <HOSTLIST> --dpi-desync=multidisorder --dpi-desync-split-pos=1,sniext+1,host+1,midsld-2,midsld,midsld+2,endhost-1";
const ZAPRET_DEFAULT_NFQWS_OPT =
"--filter-tcp=80 --dpi-desync=fake,fakedsplit --dpi-desync-autottl=2 --dpi-desync-fooling=badsum --new --filter-tcp=443 --dpi-desync=fake,multidisorder --dpi-desync-split-pos=1,midsld --dpi-desync-repeats=11 --dpi-desync-fooling=badsum --dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com --new --filter-udp=443 --dpi-desync=fake --dpi-desync-repeats=11 --dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin";
const ZAPRET2_DEFAULT_NFQWS2_OPT =
"--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000 --lua-desync=multidisorder:pos=1,midsld --new --filter-udp=443 --filter-l7=quic --payload=quic_initial --lua-desync=fake:blob=fake_default_quic:repeats=6";
const BYEDPI_DEFAULT_CMD_OPTS = "-o 2 --auto=t,r,a,s -d 2";
const ANNOTATED_TEXTAREA_STYLE_ID = "fkp-annotated-textarea-styles";
const CONNECTIONS_DYNLIST_STYLE_ID = "fkp-connections-dynlist-styles";
const NFQWS_REMOTE_VALIDATION_DEBOUNCE_MS = 500;
const NFQWS_VALIDATION_COMMAND = "/usr/bin/tachyon";
const nfqwsRemoteValidationCache = new Map();
const nfqwsRemoteValidationInflight = new Map();
const nfqws2RemoteValidationCache = new Map();
const nfqws2RemoteValidationInflight = new Map();
const byedpiRemoteValidationCache = new Map();
const byedpiRemoteValidationInflight = new Map();
const BYEDPI_LONG_VALUE_OPTIONS = new Set([
"--max-conn",
"--conn-ip",
"--buf-size",
"--debug",
"--def-ttl",
"--auto",
"--auto-mode",
"--cache-ttl",
"--cache-dump",
"--timeout",
"--proto",
"--hosts",
"--ipset",
"--pf",
"--round",
"--split",
"--disorder",
"--oob",
"--disoob",
"--fake",
"--fake-sni",
"--ttl",
"--fake-offset",
"--fake-data",
"--fake-tls-mod",
"--oob-data",
"--mod-http",
"--tlsrec",
"--tlsminor",
"--udp-fake",
]);
const BYEDPI_LONG_FLAG_OPTIONS = new Set([
"--md5sig",
"--tfo",
"--drop-sack",
"--no-domain",
"--no-udp",
]);
const BYEDPI_SHORT_VALUE_OPTIONS = new Set([
"-c",
"-I",
"-b",
"-x",
"-g",
"-A",
"-L",
"-u",
"-y",
"-T",
"-K",
"-H",
"-j",
"-V",
"-R",
"-s",
"-d",
"-o",
"-q",
"-f",
"-n",
"-t",
"-O",
"-l",
"-Q",
"-e",
"-M",
"-r",
"-m",
"-a",
]);
const BYEDPI_SHORT_FLAG_OPTIONS = new Set(["-N", "-U", "-F", "-S", "-Y"]);
const NFQWS_OPTIONAL_ARG_OPTIONS = new Set([
"--comment",
"--ctrack-disable",
"--debug",
"--dpi-desync-any-protocol",
"--dpi-desync-autottl",
"--dpi-desync-autottl6",
"--dpi-desync-skip-nosni",
"--dpi-desync-tcp-flags-set",
"--dpi-desync-tcp-flags-unset",
"--dup-autottl",
"--dup-autottl6",
"--dup-replace",
"--dup-tcp-flags-set",
"--dup-tcp-flags-unset",
"--ipcache-hostname",
"--orig-autottl",
"--orig-autottl6",
"--orig-tcp-flags-set",
"--orig-tcp-flags-unset",
"--synack-split",
]);
const NFQWS_NO_ARG_OPTIONS = new Set([
"--bind-fix4",
"--bind-fix6",
"--daemon",
"--domcase",
"--dry-run",
"--hostcase",
"--hostnospace",
"--methodeol",
"--new",
"--skip",
"--version",
]);
const NFQWS_REQUIRED_ARG_OPTIONS = new Set([
"--ctrack-timeouts",
"--dpi-desync",
"--dpi-desync-badack-increment",
"--dpi-desync-badseq-increment",
"--dpi-desync-cutoff",
"--dpi-desync-fake-dht",
"--dpi-desync-fake-discord",
"--dpi-desync-fake-http",
"--dpi-desync-fake-quic",
"--dpi-desync-fake-stun",
"--dpi-desync-fake-syndata",
"--dpi-desync-fake-tcp-mod",
"--dpi-desync-fake-tls",
"--dpi-desync-fake-tls-mod",
"--dpi-desync-fake-unknown",
"--dpi-desync-fake-unknown-udp",
"--dpi-desync-fake-wireguard",
"--dpi-desync-fakedsplit-mod",
"--dpi-desync-fakedsplit-pattern",
"--dpi-desync-fooling",
"--dpi-desync-fwmark",
"--dpi-desync-hostfakesplit-midhost",
"--dpi-desync-hostfakesplit-mod",
"--dpi-desync-ipfrag-pos-tcp",
"--dpi-desync-ipfrag-pos-udp",
"--dpi-desync-repeats",
"--dpi-desync-split-http-req",
"--dpi-desync-split-pos",
"--dpi-desync-split-seqovl",
"--dpi-desync-split-seqovl-pattern",
"--dpi-desync-split-tls",
"--dpi-desync-start",
"--dpi-desync-ts-increment",
"--dpi-desync-ttl",
"--dpi-desync-ttl6",
"--dpi-desync-udplen-increment",
"--dpi-desync-udplen-pattern",
"--dup",
"--dup-badack-increment",
"--dup-badseq-increment",
"--dup-cutoff",
"--dup-fooling",
"--dup-ip-id",
"--dup-start",
"--dup-ts-increment",
"--dup-ttl",
"--dup-ttl6",
"--filter-l3",
"--filter-l7",
"--filter-tcp",
"--filter-udp",
"--hostlist",
"--hostlist-auto",
"--hostlist-auto-debug",
"--hostlist-auto-fail-threshold",
"--hostlist-auto-fail-time",
"--hostlist-auto-retrans-threshold",
"--hostlist-domains",
"--hostlist-exclude",
"--hostlist-exclude-domains",
"--hostspell",
"--ip-id",
"--ipcache-lifetime",
"--ipset",
"--ipset-exclude",
"--ipset-exclude-ip",
"--ipset-ip",
"--orig-mod-cutoff",
"--orig-mod-start",
"--orig-ttl",
"--orig-ttl6",
"--pidfile",
"--qnum",
"--uid",
"--user",
"--wsize",
"--wssize",
"--wssize-cutoff",
"--wssize-forced-cutoff",
]);
const NFQWS2_OPTIONAL_ARG_OPTIONS = new Set([
"--chdir",
"--comment",
"--ctrack-disable",
"--debug",
"--hostlist-auto-retrans-reset",
"--intercept",
"--ipcache-hostname",
"--new",
"--payload-disable",
"--reasm-disable",
"--server",
"--template",
"--writeable",
]);
const NFQWS2_NO_ARG_OPTIONS = new Set([
"--bind-fix4",
"--bind-fix6",
"--daemon",
"--dry-run",
"--skip",
"--version",
]);
const NFQWS2_REQUIRED_ARG_OPTIONS = new Set([
"--blob",
"--cookie",
"--ctrack-timeouts",
"--filter-l3",
"--filter-l7",
"--filter-tcp",
"--filter-udp",
"--fwmark",
"--fuzz",
"--hostlist",
"--hostlist-auto",
"--hostlist-auto-debug",
"--hostlist-auto-fail-threshold",
"--hostlist-auto-fail-time",
"--hostlist-auto-retrans-threshold",
"--hostlist-domains",
"--hostlist-exclude",
"--hostlist-exclude-domains",
"--import",
"--in-range",
"--ipcache-lifetime",
"--ipset",
"--ipset-exclude",
"--ipset-exclude-ip",
"--ipset-ip",
"--lua-gc",
"--lua-init",
"--lua-desync",
"--name",
"--out-range",
"--payload",
"--pidfile",
"--qnum",
"--uid",
"--user",
]);
const actionProvidersAvailabilityState = {
loaded: false,
zapretInstalled: false,
zapret2Installed: false,
byedpiInstalled: false,
wdttInstalled: false,
olcrtcInstalled: false,
torInstalled: false,
singBoxExtended: false,
};
let actionProvidersAvailabilityPromise = null;
let actionProvidersAvailabilityLoader = null;
const outboundNameChoicesCache = new Map();
const outboundNameChoicesInflight = new Map();
const outboundTypeByNameCache = new Map();
const outboundNameSourceOptions = new Map();
const sectionGroupSourceOptions = new Map();
const dashboardFilterChoiceRefreshers = new Map();
const perRuleDnsTypeState = new Map();
const perRuleDnsWidgets = new Map();
const SECTION_CACHE_DIR = "/var/run/tachyon/section-cache";
function normalizeProtocolName(protocol) {
if (!protocol) {
return "";
}
const p = `${protocol}`.trim().toLowerCase();
switch (p) {
case "vless":
return "VLESS";
case "vmess":
return "VMess";
case "shadowsocks":
case "ss":
return "Shadowsocks";
case "trojan":
return "Trojan";
case "wireguard":
case "wg":
return "WireGuard";
case "hysteria2":
case "hy2":
return "Hysteria2";
case "hysteria":
return "Hysteria";
case "tuic":
return "TUIC";
case "socks":
case "socks5":
return "SOCKS5";
case "http":
return "HTTP";
case "direct":
return "Direct";
case "block":
return "Block";
default:
return p.charAt(0).toUpperCase() + p.slice(1);
}
}
function normalizeTransportName(transport) {
if (!transport) {
return "";
}
const t = `${transport}`.trim().toLowerCase();
switch (t) {
case "xhttp":
return "XHTTP";
case "ws":
case "websocket":
return "WS";
case "grpc":
return "gRPC";
case "http":
case "h2":
return "HTTP";
case "tcp":
case "raw":
return "TCP";
case "quic":
return "QUIC";
case "upgrade":
case "httpupgrade":
return "HTTPUpgrade";
default:
return t.toUpperCase();
}
}
function formatOutboundTypeLabel(protocol, transport) {
const normProto = normalizeProtocolName(protocol);
const normTrans = normalizeTransportName(transport);
if (!normProto && !normTrans) {
return "";
}
if (!normProto) {
return normTrans;
}
if (!normTrans) {
return normProto;
}
if (normProto.toUpperCase().includes(normTrans.toUpperCase())) {
return normProto;
}
if (["WireGuard", "Hysteria", "Hysteria2", "TUIC"].includes(normProto)) {
return normProto;
}
return `${normProto} (${normTrans})`;
}
function getProxyUrlTypeLabel(url) {
if (!url || typeof url !== "string") {
return "";
}
const trimmed = url.trim();
const schemeMatch = trimmed.match(/^([a-zA-Z0-9_-]+):\/\//);
if (!schemeMatch) {
return "";
}
const scheme = schemeMatch[1].toLowerCase();
let proto = scheme;
let transport = "";
if (scheme === "vmess") {
proto = "VMess";
try {
const b64 = trimmed.slice(8).split("#")[0];
if (typeof atob === "function") {
const jsonStr = atob(b64.replace(/[\r\n]/g, ""));
const obj = JSON.parse(jsonStr);
if (obj && typeof obj.net === "string") {
transport = obj.net;
}
}
} catch (_e) {}
} else {
try {
const [base] = trimmed.split("#");
const queryIdx = base.indexOf("?");
if (queryIdx !== -1) {
const query = base.slice(queryIdx + 1);
const params = new URLSearchParams(query);
const t = params.get("type");
if (t && t.trim()) {
transport = t.trim();
}
}
} catch (_e) {}
}
return formatOutboundTypeLabel(proto, transport);
}
const COUNTRY_CODES =
"AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW XK".split(
" ",
);
const REGION_NAME_FALLBACKS = {
XK: "Kosovo",
};
let regionDisplayNamesCache = {};
function updateActionProvidersAvailabilityState(nextState) {
if (!nextState) {
return;
}
actionProvidersAvailabilityState.loaded = true;
if (typeof nextState.zapretInstalled !== "undefined") {
actionProvidersAvailabilityState.zapretInstalled = Boolean(
nextState.zapretInstalled,
);
}
if (typeof nextState.zapret2Installed !== "undefined") {
actionProvidersAvailabilityState.zapret2Installed = Boolean(
nextState.zapret2Installed,
);
}
if (typeof nextState.byedpiInstalled !== "undefined") {
actionProvidersAvailabilityState.byedpiInstalled = Boolean(
nextState.byedpiInstalled,
);
}
if (typeof nextState.wdttInstalled !== "undefined") {
actionProvidersAvailabilityState.wdttInstalled = Boolean(
nextState.wdttInstalled,
);
}
if (typeof nextState.olcrtcInstalled !== "undefined") {
actionProvidersAvailabilityState.olcrtcInstalled = Boolean(
nextState.olcrtcInstalled,
);
}
if (typeof nextState.singBoxExtended !== "undefined") {
actionProvidersAvailabilityState.singBoxExtended = Boolean(
nextState.singBoxExtended,
);
}
if (typeof nextState.torInstalled !== "undefined") {
actionProvidersAvailabilityState.torInstalled = Boolean(
nextState.torInstalled,
);
}
actionProvidersAvailabilityPromise = null;
}
function updateActionProvidersAvailabilityFromSystemInfo(systemInfo) {
if (!systemInfo || !systemInfo.providerInfoLoaded) {
return;
}
updateActionProvidersAvailabilityState({
zapretInstalled: Boolean(systemInfo.zapret_installed),
zapret2Installed: Boolean(systemInfo.zapret2_installed),
byedpiInstalled: Boolean(systemInfo.byedpi_installed),
wdttInstalled: Boolean(systemInfo.wdtt_installed),
olcrtcInstalled: Boolean(systemInfo.olcrtc_installed),
singBoxExtended: Boolean(systemInfo.sing_box_extended),
torInstalled: Boolean(systemInfo.tor_installed),
});
}
function setActionProvidersAvailabilityLoader(loader) {
actionProvidersAvailabilityLoader =
typeof loader === "function" ? loader : null;
}
if (typeof window !== "undefined") {
window.addEventListener(ACTION_PROVIDERS_AVAILABILITY_EVENT, (event) => {
updateActionProvidersAvailabilityState(event.detail);
});
}
if (main.store && typeof main.store.subscribe === "function") {
main.store.subscribe((next, _prev, diff) => {
if (!diff || diff.diagnosticsSystemInfo) {
updateActionProvidersAvailabilityFromSystemInfo(
next.diagnosticsSystemInfo,
);
}
});
}
function getLuciLanguage() {
if (typeof L !== "undefined" && L.env && L.env.lang) {
return `${L.env.lang}`.replace("_", "-");
}
if (document.documentElement.lang) {
return document.documentElement.lang;
}
return navigator.language || "en";
}
function getRegionDisplayName(code) {
const normalizedCode = `${code || ""}`.toUpperCase();
const language = getLuciLanguage();
const cacheKey = `${language}:${normalizedCode}`;
if (regionDisplayNamesCache[cacheKey]) {
return regionDisplayNamesCache[cacheKey];
}
try {
if (typeof Intl !== "undefined" && Intl.DisplayNames) {
const displayNames = new Intl.DisplayNames([language, "en"], {
type: "region",
});
const displayName = displayNames.of(normalizedCode);
if (displayName && displayName !== normalizedCode) {
regionDisplayNamesCache[cacheKey] = displayName;
return displayName;
}
}
} catch (_error) {
// Fall through to the static fallback.
}
const fallback = REGION_NAME_FALLBACKS[normalizedCode] || normalizedCode;
regionDisplayNamesCache[cacheKey] = fallback;
return fallback;
}
function getCountryFlagEmoji(code) {
const normalizedCode = `${code || ""}`.toUpperCase();
if (!/^[A-Z]{2}$/.test(normalizedCode)) {
return "";
}
return String.fromCodePoint(
...normalizedCode
.split("")
.map((char) => 0x1f1e6 + char.charCodeAt(0) - 65),
);
}
function getCountryOptionLabel(code) {
return `${getCountryFlagEmoji(code)} ${getRegionDisplayName(code)}`;
}
function validateCountryCode(_section_id, value) {
const values = Array.isArray(value) ? value : [value];
const normalizedValues = values
.filter((item) => item && `${item}`.length)
.map((item) => `${item}`.toUpperCase());
if (!normalizedValues.length) {
return true;
}
return normalizedValues.every((item) => COUNTRY_CODES.includes(item))
? true
: _("Unknown country");
}
function plainObject(value) {
return value && typeof value === "object" && !Array.isArray(value)
? value
: {};
}
function safeCacheSectionName(section_id) {
return /^[A-Za-z0-9_-]+$/.test(`${section_id || ""}`);
}
function filteredOutboundMetadataFromCache(cache) {
const metadata = plainObject(plainObject(cache).outboundMetadata);
const names = plainObject(metadata.names);
const countries = plainObject(metadata.countries);
const protocols = plainObject(metadata.protocols);
const transports = plainObject(metadata.transports);
const candidateTags = Array.isArray(cache.urltestCandidateTags)
? cache.urltestCandidateTags
: [];
const groups = plainObject(cache.urltestGroups);
const result = {
names: {},
countries: {},
protocols: {},
transports: {},
};
if (candidateTags.length > 0) {
candidateTags.forEach((tag) => {
tag = `${tag || ""}`;
if (!tag) {
return;
}
if (names[tag] != null) {
result.names[tag] = names[tag];
}
if (countries[tag] != null) {
result.countries[tag] = countries[tag];
}
if (protocols[tag] != null) {
result.protocols[tag] = protocols[tag];
}
if (transports[tag] != null) {
result.transports[tag] = transports[tag];
}
});
return result;
}
Object.entries(names).forEach(([tag, name]) => {
if (!groups[tag]) {
result.names[tag] = name;
}
});
Object.entries(countries).forEach(([tag, country]) => {
if (!groups[tag]) {
result.countries[tag] = country;
}
});
Object.entries(protocols).forEach(([tag, protocol]) => {
if (!groups[tag]) {
result.protocols[tag] = protocol;
}
});
Object.entries(transports).forEach(([tag, transport]) => {
if (!groups[tag]) {
result.transports[tag] = transport;
}
});
return result;
}
function readOutboundMetadataFromSectionCache(section_id) {
const targetId = section_id || "Main";
if (!safeCacheSectionName(targetId)) {
return Promise.resolve({
names: {},
countries: {},
protocols: {},
transports: {},
});
}
return fs
.read(`${SECTION_CACHE_DIR}/${targetId}.json`)
.then((raw) => filteredOutboundMetadataFromCache(JSON.parse(raw || "{}")))
.catch(() => ({
names: {},
countries: {},
protocols: {},
transports: {},
}));
}
function loadOutboundNameChoices(section_id) {
const targetId = section_id || "Main";
if (outboundNameChoicesCache.has(targetId)) {
return Promise.resolve(outboundNameChoicesCache.get(targetId));
}
if (outboundNameChoicesInflight.has(targetId)) {
return outboundNameChoicesInflight.get(targetId);
}
const task = readOutboundMetadataFromSectionCache(targetId)
.then((metadata) => {
const names = Object.values(plainObject(metadata.names));
const typeMap = outboundTypeByNameCache.get(targetId) || new Map();
Object.entries(plainObject(metadata.names)).forEach(([tag, name]) => {
if (!name) {
return;
}
const proto = metadata.protocols ? metadata.protocols[tag] : "";
const trans = metadata.transports ? metadata.transports[tag] : "";
const label = formatOutboundTypeLabel(proto, trans);
if (label) {
typeMap.set(name, label);
}
});
outboundTypeByNameCache.set(targetId, typeMap);
if (targetId !== "Main") {
const mainTypeMap = outboundTypeByNameCache.get("Main") || new Map();
typeMap.forEach((v, k) => mainTypeMap.set(k, v));
outboundTypeByNameCache.set("Main", mainTypeMap);
}
const choices = names
.filter(Boolean)
.filter((name, index, values) => values.indexOf(name) === index)
.sort((a, b) => `${a}`.localeCompare(`${b}`));
outboundNameChoicesCache.set(targetId, choices);
if (targetId !== "Main") {
outboundNameChoicesCache.set("Main", choices);
}
refreshDashboardFilterChoiceWidgets(targetId);
refreshDashboardFilterChoiceWidgets("Main");
return choices;
})
.catch(() => [])
.finally(() => {
outboundNameChoicesInflight.delete(targetId);
});
outboundNameChoicesInflight.set(targetId, task);
return task;
}
function normalizeDynamicListItems(value) {
if (!value) {
return [];
}
if (Array.isArray(value)) {
return value
.map((item) => `${item || ""}`.trim())
.filter((item) => item.length);
}
const normalized = `${value}`.trim();
return normalized.length ? [normalized] : [];
}
function uniqueDynamicListItems(value) {
const seen = new Set();
const result = [];
normalizeDynamicListItems(value).forEach((item) => {
if (!seen.has(item)) {
seen.add(item);
result.push(item);
}
});
return result;
}
function readItemSettingsMap(section_id, settingsKey) {
const raw = uci.get(UCI_PACKAGE, section_id, settingsKey);
if (!raw) {
return {};
}
try {
const parsed = JSON.parse(`${raw}`);
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? parsed
: {};
} catch (_error) {
return {};
}
}
function compactItemSettings(values) {
const result = {};
Object.entries(values || {}).forEach(([key, value]) => {
if (value === undefined || value === null || value === "") {
return;
}
if (Array.isArray(value)) {
const items = value
.map((item) => `${item || ""}`.trim())
.filter((item) => item.length);
if (items.length) {
result[key] = items;
}
return;
}
result[key] = `${value}`;
});
return result;
}
function writeItemSettingsMap(section_id, settingsKey, map) {
const result = {};
Object.entries(map || {}).forEach(([item, settings]) => {
const compact = compactItemSettings(settings);
if (item && Object.keys(compact).length) {
result[item] = compact;
}
});
if (Object.keys(result).length) {
uci.set(UCI_PACKAGE, section_id, settingsKey, JSON.stringify(result));
} else {
uci.unset(UCI_PACKAGE, section_id, settingsKey);
}
}
function cleanupListItemSettings(section_id, settingsKey, values) {
if (!settingsKey) {
return;
}
const keep = new Set(normalizeDynamicListItems(values));
const map = readItemSettingsMap(section_id, settingsKey);
let changed = false;
Object.keys(map).forEach((item) => {
if (!keep.has(item)) {
delete map[item];
changed = true;
}
});
if (changed) {
writeItemSettingsMap(section_id, settingsKey, map);
}
}
function itemSettingsFlag(settings, key, defaultValue) {
const value = settings ? settings[key] : null;
if (value === undefined || value === null || value === "") {
return Boolean(defaultValue);
}
return value === true || `${value}` === "1";
}
function ensureConnectionsDynamicListStyles() {
if (document.getElementById(CONNECTIONS_DYNLIST_STYLE_ID)) {
return;
}
document.head.appendChild(
E(
"style",
{ id: CONNECTIONS_DYNLIST_STYLE_ID },
`
.fkp-connections-dynlist > .item {
--fkp-dynlist-action-width: 2em;
padding-right: calc(var(--fkp-dynlist-action-width) * 2);
position: relative;
}
.fkp-connections-dynlist > .item > .fkp-dynlist-settings {
align-items: center;
border: 1px solid var(--border-color-high, currentColor);
border-right: 0;
border-radius: 0;
bottom: -1px;
color: inherit;
cursor: pointer;
display: inline-flex;
font: inherit;
font-size: 0.9em;
justify-content: center;
line-height: 1;
min-height: 0;
padding: 0;
pointer-events: auto;
position: absolute;
right: calc(var(--fkp-dynlist-action-width) - 1px);
user-select: none;
text-decoration: none;
top: -1px;
width: var(--fkp-dynlist-action-width);
z-index: 1;
}
.fkp-connections-dynlist > .item > .fkp-dynlist-settings:hover,
.fkp-connections-dynlist > .item > .fkp-dynlist-settings:focus {
--focus-color-rgb: 82, 168, 236;
outline: 0;
border-color: rgba(var(--focus-color-rgb), 0.8) !important;
box-shadow: inset 0 1px 3px hsla(var(--border-color-low-hsl), .01), 0 0 8px rgba(var(--focus-color-rgb), 0.6);
text-decoration: none;
}
.fkp-connections-dynlist > .add-item > .cbi-dropdown {
width: 100%;
}
.fkp-interface-dynlist-label {
align-items: center;
display: inline-flex;
gap: 0.25em;
max-width: 100%;
vertical-align: middle;
}
.fkp-interface-dynlist-label > img {
flex: 0 0 auto;
height: 1.35em;
width: auto;
}
.fkp-interface-dynlist-label > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fkp-button-add-dynlist > .add-item {
align-items: stretch;
background: transparent;
border: 0;
border-radius: 0;
box-shadow: none;
display: flex;
margin-top: 4px;
max-width: 100%;
overflow: visible;
padding: 0;
width: var(--fkp-button-add-width, 210px);
}
.fkp-button-add-dynlist > .add-item > input[type="text"] {
display: none !important;
}
.fkp-button-add-dynlist > .add-item > .cbi-button-add {
align-items: center !important;
background: linear-gradient(var(--background-color-high) 0%, var(--border-color-low) 100%) !important;
border: 1px solid var(--border-color-high) !important;
border-radius: 3px !important;
box-shadow: inset 0 1px 3px hsla(var(--border-color-low-hsl), .01) !important;
box-sizing: border-box !important;
color: var(--text-color-medium) !important;
cursor: pointer !important;
display: flex !important;
font-size: 13px !important;
height: 30px !important;
justify-content: flex-start !important;
margin-left: 0 !important;
max-height: 30px !important;
min-height: 30px !important;
max-width: 100% !important;
overflow: hidden !important;
padding: 0 4px !important;
text-overflow: ellipsis !important;
transition: border linear .2s, box-shadow linear .2s !important;
white-space: nowrap !important;
width: 100% !important;
}
.fkp-button-add-dynlist > .add-item > .cbi-button-add:hover,
.fkp-button-add-dynlist > .add-item > .cbi-button-add:focus {
border-color: rgba(82, 168, 236, .8) !important;
box-shadow: inset 0 1px 3px hsla(var(--border-color-low-hsl), .01), 0 0 8px rgba(82, 168, 236, .6) !important;
outline: 0;
}
body.modal-overlay-active > #modal_overlay > .modal.cbi-modal > .cbi-map.flash {
animation: none !important;
}
`,
),
);
}
function findDynamicListItemByValue(dl, value) {
const stringValue = `${value}`;
const items = dl.querySelectorAll(".item");
for (let i = 0; i < items.length; i += 1) {
const hidden = items[i].querySelector('input[type="hidden"]');
if (
hidden &&
hidden.parentNode === items[i] &&
hidden.value === stringValue
) {
return items[i];
}
}
return null;
}
function dynamicListItemCurrentValue(item, fallback) {
const hidden = item ? item.querySelector('input[type="hidden"]') : null;
return hidden && hidden.parentNode === item ? hidden.value : fallback;
}
function dynamicListItemValues(dl) {
return Array.from(dl.querySelectorAll(".item"))
.map((item) => {
const hidden = item.querySelector('input[type="hidden"]');
return hidden && hidden.parentNode === item ? hidden.value : null;
})
.filter((value) => value != null);
}
function updateDynamicListItemLabel(item, label) {
const labelNode = item ? item.querySelector(".fkp-dynlist-label") : null;
if (labelNode) {
labelNode.textContent = `${label || ""}`;
}
}
function addDynamicListItem(widget, value, text) {
if (!widget || !widget.node) {
return null;
}
const normalized = `${value || ""}`.trim();
if (!normalized) {
return null;
}
if (
typeof widget.options.hasEquivalentValue === "function" &&
widget.options.hasEquivalentValue(normalized, widget.node)
) {
widget.dispatchCbiDynlistChange(widget.node, normalized);
return null;
}
widget.addItem(widget.node, normalized, text || null, false);
widget.dispatchCbiDynlistChange(widget.node, normalized);
updateButtonAddDynamicListLayout(widget.node, widget.options.addButtonLabel);
return findDynamicListItemByValue(widget.node, normalized);
}
function updateButtonAddDynamicListLayout(dl, label) {
const addButton = dl ? dl.querySelector(".add-item > .cbi-button-add") : null;
if (!addButton) {
return;
}
addButton.textContent = label || "+";
addButton.setAttribute("role", "button");
addButton.setAttribute("tabindex", "0");
dl.style.setProperty(
"--fkp-button-add-width",
dl.querySelector(".item") ? "100%" : "210px",
);
}
function setDynamicListItemValue(item, value) {
const hidden = item ? item.querySelector('input[type="hidden"]') : null;
if (hidden && hidden.parentNode === item) {
hidden.value = `${value || ""}`;
}
}
function parseOutboundJsonObject(value) {
try {
const parsed = JSON.parse(`${value || ""}`);
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
? parsed
: null;
} catch (_error) {
return null;
}
}
function outboundJsonDisplayTag(value) {
const parsed = parseOutboundJsonObject(value);
const tag = parsed && typeof parsed.tag === "string" ? parsed.tag.trim() : "";
return tag || "";
}
function outboundJsonListItemLabel(value) {
return E(
"span",
{ class: "fkp-dynlist-label" },
outboundJsonDisplayTag(value) || _("JSON outbound"),
);
}
function cleanFormSectionData(sectionData) {
const result = {};
Object.entries(sectionData || {}).forEach(([key, value]) => {
if (!key || key.charAt(0) === ".") {
return;
}
result[key] = Array.isArray(value) ? value.slice() : value;
});
return result;
}
const SettingsUIDynamicList = ui.DynamicList.extend({
render() {
ensureConnectionsDynamicListStyles();
const node = ui.DynamicList.prototype.render.apply(this, arguments);
node.classList.add("fkp-connections-dynlist");
return node;
},
addItem(dl, value, text, flash) {
if (
flash &&
typeof this.options.hasEquivalentValue === "function" &&
this.options.hasEquivalentValue(value, dl)
) {
this.dispatchCbiDynlistChange(dl, value);
return;
}
const itemText =
typeof this.options.itemLabel === "function"
? this.options.itemLabel(value, text)
: text;
ui.DynamicList.prototype.addItem.call(this, dl, value, itemText, flash);
const item = findDynamicListItemByValue(dl, value);
const hasSettings =
typeof this.options.hasSettings === "function"
? this.options.hasSettings(value)
: true;
if (!item || item.querySelector(".fkp-dynlist-settings") || !hasSettings) {
return;
}
item.appendChild(
E(
"span",
{
role: "button",
tabindex: this.options.disabled ? null : "0",
class: "fkp-dynlist-settings",
"aria-label": _("Settings"),
"aria-disabled": this.options.disabled ? "true" : null,
click: (event) => {
event.preventDefault();
event.stopPropagation();
if (this.options.disabled) {
return;
}
if (typeof this.options.settingsHandler === "function") {
this.options.settingsHandler(
dynamicListItemCurrentValue(item, value),
item,
this,
{},
);
}
},
keydown: (event) => {
if (event.key !== "Enter" && event.key !== " ") {
return;
}
event.preventDefault();
event.stopPropagation();
if (
!this.options.disabled &&
typeof this.options.settingsHandler === "function"
) {
this.options.settingsHandler(
dynamicListItemCurrentValue(item, value),
item,
this,
{},
);
}
},
},
"\u2699",
),
);
},
handleClick(event) {
if (event.target.closest(".fkp-dynlist-settings")) {
return;
}
return ui.DynamicList.prototype.handleClick.apply(this, arguments);
},
});
const ButtonAddSettingsUIDynamicList = SettingsUIDynamicList.extend({
render() {
const node = SettingsUIDynamicList.prototype.render.apply(this, arguments);
node.classList.add("fkp-button-add-dynlist");
const input = node.querySelector('.add-item > input[type="text"]');
if (input) {
input.setAttribute("aria-hidden", "true");
input.setAttribute("tabindex", "-1");
}
updateButtonAddDynamicListLayout(node, this.options.addButtonLabel);
node.addEventListener("cbi-dynlist-change", () => {
updateButtonAddDynamicListLayout(node, this.options.addButtonLabel);
});
return node;
},
addButtonItem() {
if (typeof this.options.settingsHandler === "function") {
this.options.settingsHandler("", null, this, { adding: true });
}
},
handleClick(event) {
if (
!this.options.disabled &&
event.target.closest(".add-item > .cbi-button-add")
) {
event.preventDefault();
event.stopPropagation();
this.addButtonItem(event.currentTarget);
return;
}
return SettingsUIDynamicList.prototype.handleClick.apply(this, arguments);
},
handleKeydown(event) {
if (
!this.options.disabled &&
event.target.closest(".add-item > .cbi-button-add") &&
(event.key === "Enter" || event.key === " ")
) {
event.preventDefault();
event.stopPropagation();
this.addButtonItem(event.currentTarget);
return;
}
return ui.DynamicList.prototype.handleKeydown.apply(this, arguments);
},
});
const SettingsDynamicList = form.DynamicList.extend({
childOwner(section_id) {
return typeof this.childOwnerId === "function"
? `${this.childOwnerId(section_id) || ""}`.trim()
: section_id;
},
parentSection(section_id) {
return typeof this.parentSectionId === "function"
? `${this.parentSectionId(section_id) || ""}`.trim()
: section_id;
},
load(section_id) {
if (this.childType) {
return getChildItemIds(
this.childOwner(section_id),
this.childType,
this.ownerOption,
);
}
return form.DynamicList.prototype.load.apply(this, arguments);
},
renderWidget(section_id, _option_index, cfgvalue) {
const value = cfgvalue != null ? cfgvalue : this.default;
const choices = this.transformChoices();
const WidgetClass = this.buttonAdd
? ButtonAddSettingsUIDynamicList
: SettingsUIDynamicList;
const widget = new WidgetClass(L.toArray(value), choices, {
id: this.cbid(section_id),
sort: this.keylist,
allowduplicates: this.allowduplicates,
optional: this.optional || this.rmempty,
datatype: this.datatype,
placeholder: this.placeholder,
validate: L.bind(this.validate, this, section_id),
disabled: this.readonly != null ? this.readonly : this.map.readonly,
addButtonLabel:
typeof this.addButtonLabel === "function"
? this.addButtonLabel(section_id)
: this.addButtonLabel,
settingsHandler: (itemValue, _item, widget, context) => {
if (typeof this.renderItemSettingsModal === "function") {
const ownerId = this.childOwner(section_id);
this.renderItemSettingsModal(
ownerId,
`${itemValue}`,
this,
widget,
_item,
Object.assign({}, context || {}, {
parentSectionId: this.parentSection(section_id),
ownerId,
}),
);
}
},
itemLabel: (itemValue, text) => {
if (typeof this.renderListItemLabel === "function") {
return this.renderListItemLabel(
this.childOwner(section_id),
`${itemValue}`,
text,
);
}
return text;
},
hasSettings: (itemValue) => {
if (typeof this.hasItemSettings === "function") {
return this.hasItemSettings(
this.childOwner(section_id),
`${itemValue}`,
);
}
if (this.childType) {
return isExistingChildItem(
this.childOwner(section_id),
`${itemValue}`,
this.childType,
this.ownerOption,
);
}
return true;
},
hasEquivalentValue: (itemValue, dl) => {
const inputValueForItem = (value) => {
const ownerId = this.childOwner(section_id);
if (typeof this.inputValueForItem === "function") {
return `${this.inputValueForItem(ownerId, `${value || ""}`) || ""}`.trim();
}
if (this.childType && this.childValueOption) {
return childItemInputValue(
ownerId,
`${value || ""}`,
this.childType,
this.childValueOption,
this.ownerOption,
);
}
return `${value || ""}`.trim();
};
const normalized = inputValueForItem(itemValue);
return Boolean(
normalized &&
dynamicListItemValues(dl).some(
(existingValue) =>
inputValueForItem(existingValue) === normalized,
),
);
},
});
const node = widget.render();
if (typeof this.onListChange === "function") {
node.addEventListener("cbi-dynlist-change", () => {
this.onListChange(section_id);
});
}
return node;
},
parse(section_id) {
if (
this.isActive(section_id) &&
typeof this.validateItemsOnSave === "function"
) {
const result = this.validateItemsOnSave(
this.childType ? this.childOwner(section_id) : section_id,
this.formvalue(section_id),
this,
section_id,
);
if (result !== true) {
const title = this.stripTags(this.title).trim();
return Promise.reject(
new TypeError(
`${_('Option "%s" contains an invalid input value.').format(title || this.option)} ${result}`,
),
);
}
}
return form.DynamicList.prototype.parse.apply(this, arguments);
},
write(section_id, value) {
if (this.childType) {
const ownerId = this.childOwner(section_id);
const itemIds = materializeChildItems(
ownerId,
{
typeName: this.childType,
valueOption: this.childValueOption,
ownerOption: this.ownerOption,
createId: this.createId,
defaults: this.childDefaults,
stagedSettings:
typeof this.stagedChildSettings === "function"
? (itemValue, itemId, created) =>
this.stagedChildSettings(ownerId, itemValue, itemId, created)
: null,
},
value,
);
cleanupRemovedChildItems(
ownerId,
this.childType,
itemIds,
this.ownerOption,
);
itemIds.forEach((itemId, index) => {
uci.set(UCI_PACKAGE, itemId, "order", `${index}`);
});
if (typeof uci.reorder === "function") {
uci.reorder(UCI_PACKAGE, itemIds);
}
if (typeof this.afterMaterializeChildItems === "function") {
this.afterMaterializeChildItems(ownerId, itemIds);
}
uci.unset(UCI_PACKAGE, section_id, this.option);
cleanupListItemSettings(ownerId, this.settingsKey, itemIds);
if (typeof this.clearStagedChildSettings === "function") {
this.clearStagedChildSettings(ownerId);
}
return;
}
const result = form.DynamicList.prototype.write.apply(this, arguments);
cleanupListItemSettings(section_id, this.settingsKey, value);
return result;
},
remove(section_id) {
if (this.childType) {
cleanupRemovedChildItems(
this.childOwner(section_id),
this.childType,
[],
this.ownerOption,
);
uci.unset(UCI_PACKAGE, section_id, this.option);
return;
}
if (this.settingsKey) {
uci.unset(UCI_PACKAGE, section_id, this.settingsKey);
}
return form.DynamicList.prototype.remove.apply(this, arguments);
},
});
const ButtonAddSettingsDynamicList = SettingsDynamicList.extend({
buttonAdd: true,
});
function configureLiveDynamicListChoices(option, getChoices) {
option.renderWidget = function (section_id, _option_index, cfgvalue) {
const values = L.toArray(cfgvalue != null ? cfgvalue : this.default);
const choices = getChoices(section_id, values);
const labels = {};
choices.forEach((choice) => {
labels[choice.value] = choice.label;
});
refreshOptionChoices(this, choices);
let choiceSignature = JSON.stringify(
choices.map((choice) => [choice.value, choice.label]),
);
// form.DynamicList passes null when there are no choices, which permanently
// renders a plain input. An empty object keeps the stock combobox alive so
// later live choices can be added without reopening the modal.
const widget = new ui.DynamicList(values, labels, {
id: this.cbid(section_id),
sort: this.keylist,
allowduplicates: this.allowduplicates,
optional: this.optional || this.rmempty,
datatype: this.datatype,
placeholder: this.placeholder,
validate: L.bind(this.validate, this, section_id),
disabled: this.readonly != null ? this.readonly : this.map.readonly,
});
const node = widget.render();
const refreshChoices = () => {
if (!node.isConnected) {
return false;
}
const currentValues = widget.getValue();
const currentChoices = getChoices(section_id, currentValues);
const currentLabels = {};
currentChoices.forEach((choice) => {
currentLabels[choice.value] = choice.label;
});
const currentSignature = JSON.stringify(
currentChoices.map((choice) => [choice.value, choice.label]),
);
if (currentSignature === choiceSignature) {
return;
}
choiceSignature = currentSignature;
refreshOptionChoices(this, currentChoices);
widget.choices = currentLabels;
widget.clearChoices();
widget.addChoices(
currentChoices.map((choice) => choice.value),
currentLabels,
);
const cboxElem = node.querySelector(".cbi-dropdown");
if (
cboxElem &&
typeof dom !== "undefined" &&
typeof dom.callClassMethod === "function"
) {
try {
dom.callClassMethod(cboxElem, "clearChoices");
dom.callClassMethod(
cboxElem,
"addChoices",
currentChoices.map((choice) => choice.value),
currentLabels,
);
} catch (_err) {}
}
return true;
};
const refreshBeforeOpening = () => {
refreshChoices();
};
node.addEventListener("mousedown", refreshBeforeOpening, true);
node.addEventListener("focusin", refreshBeforeOpening, true);
node.addEventListener("pointerdown", refreshBeforeOpening, true);
if (!dashboardFilterChoiceRefreshers.has(section_id)) {
dashboardFilterChoiceRefreshers.set(section_id, new Set());
}
dashboardFilterChoiceRefreshers.get(section_id).add(refreshChoices);
const targetSectionId = parentSectionIdForItem(section_id) || "Main";
if (targetSectionId && targetSectionId !== section_id) {
if (!dashboardFilterChoiceRefreshers.has(targetSectionId)) {
dashboardFilterChoiceRefreshers.set(targetSectionId, new Set());
}
dashboardFilterChoiceRefreshers.get(targetSectionId).add(refreshChoices);
}
perRuleDnsWidgets.set(section_id, widget);
loadOutboundNameChoices(targetSectionId).then(() => {
refreshChoices();
});
return node;
};
}
function countryChoices() {
return COUNTRY_CODES.map((code) => ({
value: code,
label: getCountryOptionLabel(code),
})).sort((a, b) => a.label.localeCompare(b.label));
}
function getOutboundTypeForName(section_id, name) {
if (!name) {
return "";
}
const targetId = section_id || "Main";
const typeMap = outboundTypeByNameCache.get(targetId);
if (typeMap && typeMap.has(name)) {
return typeMap.get(name);
}
if (targetId !== "Main") {
const mainMap = outboundTypeByNameCache.get("Main");
if (mainMap && mainMap.has(name)) {
return mainMap.get(name);
}
}
for (const map of outboundTypeByNameCache.values()) {
if (map && typeof map.get === "function" && map.has(name)) {
return map.get(name);
}
}
let found = "";
currentSourceOptionValues(section_id, "selector_proxy_links").forEach(
(link) => {
if (found) {
return;
}
const linkName = main.getProxyUrlName(`${link || ""}`);
if (linkName === name) {
found = getProxyUrlTypeLabel(link);
}
},
);
if (found) {
return found;
}
currentSourceOptionValues(section_id, "outbound_jsons").forEach((jsonStr) => {
if (found) {
return;
}
const parsed = parseOutboundJsonObject(jsonStr);
if (parsed && (parsed.tag || "") === name) {
const proto = parsed.type || "";
const trans = (parsed.transport && parsed.transport.type) || "";
found = formatOutboundTypeLabel(proto, trans);
}
});
return found;
}
function currentOutboundNameChoices(section_id, values) {
if (!outboundNameChoicesCache.has(section_id)) {
loadOutboundNameChoices(section_id);
}
const seen = new Set();
const result = [];
const append = (name) => {
const value = `${name || ""}`.trim();
if (!value || seen.has(value)) {
return;
}
seen.add(value);
const typeStr = getOutboundTypeForName(section_id, value);
const label =
typeStr && !value.endsWith(`[${typeStr}]`)
? `${value} [${typeStr}]`
: value;
result.push({ value, label });
};
(outboundNameChoicesCache.get(section_id) || []).forEach(append);
currentDraftOutboundNames(section_id).forEach(append);
normalizeDynamicListItems(values).forEach(append);
return result.sort((a, b) => a.label.localeCompare(b.label));
}
function currentSourceOptionValues(section_id, optionName) {
const liveValues = currentLiveDynamicListValues(section_id, optionName);
if (liveValues != null) {
return liveValues;
}
const option = outboundNameSourceOptions.get(optionName);
if (option && typeof option.formvalue === "function") {
try {
const value = option.formvalue(section_id);
if (value != null) {
return normalizeDynamicListItems(value);
}
} catch (_error) {
// Fall back to the LuCI UCI cache when the section widget is not mounted.
}
}
return getConfigListValues(section_id, optionName);
}
function currentLiveDynamicListValues(section_id, optionName) {
if (typeof document === "undefined") {
return null;
}
const widget = document.getElementById(
`cbid.${UCI_PACKAGE}.${section_id}.${optionName}`,
);
if (!widget) {
return null;
}
const values = Array.from(
widget.querySelectorAll('.item > input[type="hidden"]'),
)
.map((input) => `${input.value || ""}`.trim())
.filter(Boolean);
const pendingInput = widget.querySelector('.add-item > input[type="text"]');
const pendingValue = `${(pendingInput && pendingInput.value) || ""}`.trim();
if (
pendingValue &&
!pendingInput.classList.contains("cbi-input-invalid") &&
!values.includes(pendingValue)
) {
values.push(pendingValue);
}
return values;
}
function currentDraftOutboundNames(section_id) {
const names = [];
currentSourceOptionValues(section_id, "selector_proxy_links").forEach(
(value, index) => {
const name = main.getProxyUrlName(`${value || ""}`);
names.push(name || `${section_id}-${index + 1}-out`);
},
);
currentSourceOptionValues(section_id, "interfaces").forEach((itemId) => {
const normalized = childItemInputValue(
section_id,
itemId,
"section_interface",
"name",
).trim();
if (normalized) {
names.push(normalized);
}
});
currentSourceOptionValues(section_id, "outbound_jsons").forEach((value) => {
const tag = outboundJsonDisplayTag(value);
if (tag) {
names.push(tag);
}
});
return names;
}
function currentSectionGroupValues(section_id, typeName) {
const liveValues = currentLiveDynamicListValues(section_id, typeName);
if (liveValues != null) {
return liveValues;
}
const option = sectionGroupSourceOptions.get(typeName);
if (option && typeof option.formvalue === "function") {
try {
const value = option.formvalue(section_id);
if (value != null) {
return normalizeDynamicListItems(value);
}
} catch (_error) {
// Fall back to the child sections when the widget is not mounted.
}
}
return getChildItemIds(section_id, typeName);
}
function sectionGroupDisplayName(section_id, typeName, value) {
const itemId = `${value || ""}`.trim();
if (isExistingChildItem(section_id, itemId, typeName)) {
return `${uci.get(UCI_PACKAGE, itemId, "name") || itemId}`.trim();
}
const option = sectionGroupSourceOptions.get(typeName);
const pending =
option && option.pendingChildSettings
? option.pendingChildSettings[section_id]
: null;
const settings = pending && pending[itemId];
return `${(settings && settings.name) || itemId}`.trim();
}
function currentSectionGroupChoices(section_id, selectedValues = []) {
const seen = new Set();
const result = [];
const append = (typeName, value) => {
value = `${value || ""}`.trim();
if (!value) {
return;
}
const name = sectionGroupDisplayName(section_id, typeName, value) || value;
if (seen.has(name)) {
return;
}
seen.add(name);
result.push({ value: name, label: name });
};
currentSectionGroupValues(section_id, "urltest").forEach((value) =>
append("urltest", value),
);
currentSectionGroupValues(section_id, "priority_group").forEach((value) =>
append("priority_group", value),
);
normalizeDynamicListItems(selectedValues).forEach((value) => {
value = `${value || ""}`.trim();
if (value && !seen.has(value)) {
seen.add(value);
result.push({ value, label: value });
}
});
return result.sort((left, right) => left.label.localeCompare(right.label));
}
function refreshDashboardFilterChoiceWidgets(section_id) {
const refreshers = dashboardFilterChoiceRefreshers.get(section_id);
if (!refreshers) {
return;
}
refreshers.forEach((refresh) => {
if (refresh() === false) {
refreshers.delete(refresh);
}
});
if (refreshers.size === 0) {
dashboardFilterChoiceRefreshers.delete(section_id);
}
}
function isDownloadThroughTargetSection(section, currentSectionId) {
const sectionName = getUciSectionName(section);
const action = (section && section.action) || "";
if (
!sectionName ||
section.enabled === "0"
) {
return false;
}
if (
[
"connection",
"proxy",
"outbound",
"vpn",
"awg",
"warp",
"anytls",
"snell",
"mieru",
"sudoku",
"masque",
"openvpn",
].includes(action)
) {
return true;
}
if (action === "zapret") {
return isZapretInstalledForUi();
}
if (action === "zapret2") {
return isZapret2InstalledForUi();
}
if (action === "byedpi") {
return isByedpiInstalledForUi();
}
if (action === "wdtt") {
return isWdttInstalledForUi();
}
if (action === "olcrtc") {
return isOlcrtcInstalledForUi();
}
return false;
}
function subscriptionDownloadTargetChoices(section_id) {
return (uci.sections(UCI_PACKAGE, "section") || [])
.filter((section) => isDownloadThroughTargetSection(section, section_id))
.map((section) => {
const name = getUciSectionName(section);
let label = getUciSectionLabel(section);
if (name === section_id) {
label += " " + _("(this rule, when active)");
}
return {
value: name,
label: label,
};
});
}
function dnsTypeChoices() {
return [
{ value: "doh", label: _("DNS over HTTPS (DoH)") },
{ value: "dot", label: _("DNS over TLS (DoT)") },
{ value: "doq", label: _("DNS over QUIC (DoQ)") },
{ value: "udp", label: "UDP" },
];
}
function optionListValues(option, section_id) {
const value = option.cfgvalue
? option.cfgvalue(section_id)
: uci.get(UCI_PACKAGE, section_id, option.option);
return L.toArray(value)
.map((item) => `${item || ""}`.trim())
.filter(Boolean);
}
function configureDnsList(option, choices, defaultValue) {
if (choices) {
Object.entries(choices).forEach(([key, label]) => {
option.value(key, _(label));
});
}
option.default = [defaultValue];
option.rmempty = false;
option.validate = function (_section_id, value) {
const normalized = `${value || ""}`.trim();
if (!normalized) {
return optionListValues(option, _section_id).length > 0
? true
: _("Add at least one DNS server");
}
const validation = main.validateDNS(normalized);
return validation.valid ? true : validation.message;
};
}
function isConnectionNetworkInterfaceAllowed(deviceName, device) {
if (CONNECTIONS_BLOCKED_INTERFACES.includes(deviceName)) {
return false;
}
if (!device) {
return true;
}
const type = device.getType();
const isWireless =
type === "wifi" || type === "wireless" || type.indexOf("wlan") >= 0;
return !isWireless;
}
function renderNetworkInterfaceChoice(device) {
const name = device.getName();
const type = device.getType();
return E([
E("img", {
title: device.getI18n(),
src: L.resource(
"icons/%s%s.svg".format(type, device.isUp() ? "" : "_disabled"),
),
}),
E("span", { class: "hide-open" }, [name]),
E("span", { class: "hide-close" }, [device.getI18n()]),
]);
}
function renderNetworkInterfaceListItem(device, fallbackName) {
const name = device ? device.getName() : fallbackName;
const type = device ? device.getType() : "ethernet";
const up = device ? device.isUp() : false;
return E("span", { class: "fkp-interface-dynlist-label" }, [
E("img", {
title: device ? device.getI18n() : _("Network Interface"),
src: L.resource("icons/%s%s.svg".format(type, up ? "" : "_disabled")),
}),
E("span", {}, [name]),
]);
}
function refreshNetworkInterfaceOptionValues(option) {
option.keylist = [];
option.vallist = [];
option.interfaceChoiceMap = {};
option.interfaceDeviceMap = {};
(option.devices || []).forEach((device) => {
const name = device.getName();
const type = device.getType();
if (
name === "lo" ||
type === "alias" ||
!isConnectionNetworkInterfaceAllowed(name, device)
) {
return;
}
option.value(name, renderNetworkInterfaceChoice(device));
option.interfaceChoiceMap[name] = true;
option.interfaceDeviceMap[name] = device;
});
}
const InterfaceSettingsDynamicList = SettingsDynamicList.extend({
load(section_id) {
return network.getDevices().then(
L.bind(function (devices) {
this.devices = devices || [];
refreshNetworkInterfaceOptionValues(this);
return this.super("load", section_id);
}, this),
);
},
validate(section_id, value) {
value = childItemInputValue(section_id, value, "section_interface", "name");
if (!value || value.length === 0) {
return true;
}
if (!this.interfaceChoiceMap || !this.interfaceChoiceMap[value]) {
return _("Select an existing network interface");
}
return true;
},
renderListItemLabel(section_id, value, text) {
value = childItemInputValue(section_id, value, "section_interface", "name");
return renderNetworkInterfaceListItem(
this.interfaceDeviceMap ? this.interfaceDeviceMap[value] : null,
value || text,
);
},
});
function urlTestFilterModeChoices() {
return [
{ value: "disabled", label: _("All servers") },
{ value: "exclude", label: _("All except selected") },
{ value: "include", label: _("Only selected") },
{ value: "mixed", label: _("Only selected except exclusions") },
];
}
function priorityLevelFilterModeChoices() {
return [
{ value: "disabled", label: _("All remaining servers") },
{ value: "include", label: _("Only selected") },
{ value: "exclude", label: _("All remaining except selected") },
{ value: "mixed", label: _("Only selected except exclusions") },
];
}
function serverCountryDetectionChoices() {
return [
{ value: "flag_emoji", label: _("By flag emoji from name") },
{ value: "country_is", label: _("Via country.is") },
];
}
function proxyProtocolChoices() {
return [
["vless", "VLESS"],
["vmess", "VMess"],
["trojan", "Trojan"],
["shadowsocks", "Shadowsocks"],
["socks", "SOCKS"],
["http", "HTTP"],
["hysteria2", "Hysteria2"],
["direct", "Direct"],
];
}
function proxyTransportChoices() {
return [
["tcp", "TCP"],
["ws", "WebSocket"],
["grpc", "gRPC"],
["http", "HTTP"],
["httpupgrade", "HTTPUpgrade"],
["xhttp", "XHTTP"],
];
}
function proxySecurityChoices() {
return [
["none", "None"],
["tls", "TLS"],
["reality", "Reality"],
];
}
function addProxyParameterFilterOptions(itemSection, options) {
const prefix = options.prefix;
const dependencies = options.dependencies;
let o = itemSection.option(
form.Flag,
`${prefix}_proxy_parameters`,
options.toggleLabel,
options.toggleDescription,
);
dependencies.forEach((dependency) => o.depends(dependency));
o.default = "0";
o.rmempty = false;
[
[
"protocols",
_("Protocol"),
options.protocolDescription,
proxyProtocolChoices(),
],
[
"transports",
_("Transport"),
options.transportDescription,
proxyTransportChoices(),
],
[
"securities",
_("Security"),
options.securityDescription,
proxySecurityChoices(),
],
].forEach(([suffix, label, description, choices]) => {
const list = itemSection.option(
form.DynamicList,
`${prefix}_${suffix}`,
label,
description,
);
dependencies.forEach((dependency) =>
list.depends(
Object.assign({}, dependency, {
[`${prefix}_proxy_parameters`]: "1",
}),
),
);
list.rmempty = true;
choices.forEach(([value, choiceLabel]) => list.value(value, choiceLabel));
list.placeholder = _("-- Select --");
});
}
function urlTestUrlChoices() {
return Array.isArray(main.LATENCY_TEST_URL_OPTIONS)
? main.LATENCY_TEST_URL_OPTIONS
: [main.DEFAULT_LATENCY_TEST_URL || "https://www.gstatic.com/generate_204"];
}
function validateUrlTestTolerance(value) {
if (!value || `${value}`.length === 0) {
return _("Must be a number in the range of 0 - 10000");
}
const normalized = `${value}`;
const parsed = parseFloat(normalized);
if (
/^[0-9]+$/.test(normalized) &&
!isNaN(parsed) &&
isFinite(parsed) &&
parsed >= 0 &&
parsed <= 10000
) {
return true;
}
return _("Must be a number in the range of 0 - 10000");
}
function validateUrlTestUrl(value) {
const validation = main.validateUrl(`${value || ""}`.trim());
return validation.valid ? true : validation.message;
}
function generateHwid16() {
const bytes = new Uint8Array(8);
if (window.crypto && window.crypto.getRandomValues) {
window.crypto.getRandomValues(bytes);
} else {
for (let i = 0; i < bytes.length; i += 1) {
bytes[i] = Math.floor(Math.random() * 256);
}
}
return Array.from(bytes)
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
function optionMapValue(option, section_id, key) {
const value =
option && option.map && option.map.data
? option.map.data.get(option.map.config, section_id, key)
: uci.get(UCI_PACKAGE, section_id, key);
return value == null ? "" : value;
}
function subscriptionUrlSettingsKeys() {
return [
"subscription_update_enabled",
"subscription_update_interval",
"download_via_proxy_enabled",
"download_via_proxy_section",
"auto_user_agent",
"user_agent",
"auto_hwid",
"hwid",
"custom_device_headers",
"device_os",
"ver_os",
"device_model",
"device_locale",
"app_version",
"accept_language",
"show_dashboard_metadata",
"prefix_nodes",
"node_prefix",
"include_urltest_groups",
"hide_urltest_group_outbounds",
"hide_detour_outbounds",
"subscription_insecure",
];
}
function defaultSubscriptionUrlSettings() {
return {
subscription_update_enabled: "1",
subscription_update_interval: "1h",
download_via_proxy_enabled: "0",
download_via_proxy_section: "",
auto_user_agent: "1",
user_agent: "",
auto_hwid: "1",
hwid: "",
custom_device_headers: "0",
device_os: "",
ver_os: "",
device_model: "",
device_locale: "",
app_version: "",
accept_language: "",
show_dashboard_metadata: "1",
prefix_nodes: "0",
node_prefix: "",
include_urltest_groups: "1",
hide_urltest_group_outbounds: "1",
hide_detour_outbounds: "1",
subscription_insecure: "0",
};
}
function interfaceSettingsKeys() {
return [
"domain_resolver_enabled",
"domain_resolver_dns_type",
"domain_resolver_dns_server",
];
}
function defaultInterfaceSettings() {
return {
domain_resolver_enabled: "0",
domain_resolver_dns_type: "udp",
domain_resolver_dns_server: "8.8.8.8",
};
}
function subscriptionUserAgentChoices() {
return [
"sing-box",
"Happ",
"v2rayN",
"v2rayNG",
"v2RayTun",
"Incy",
"Hiddify",
"HiddifyNext",
"Clash",
"Clash.Meta",
"ClashMetaForAndroid",
"Mihomo",
"NekoBox",
"Karing",
"Husi",
];
}
function urlTestSettingsKeys() {
return [
"name",
"check_interval",
"tolerance",
"testing_url",
"idle_timeout",
"interrupt_exist_connections",
"pin_dashboard",
"filter_mode",
"detect_server_country",
"include_countries",
"include_outbounds",
"include_regex",
"include_proxy_parameters",
"include_protocols",
"include_transports",
"include_securities",
"exclude_countries",
"exclude_outbounds",
"exclude_regex",
"exclude_proxy_parameters",
"exclude_protocols",
"exclude_transports",
"exclude_securities",
];
}
function defaultUrlTestSettings(name) {
return {
name: "",
check_interval: "3m",
tolerance: "50",
testing_url: "https://www.gstatic.com/generate_204",
idle_timeout: "30m",
interrupt_exist_connections: "1",
pin_dashboard: "1",
filter_mode: "disabled",
detect_server_country: "flag_emoji",
};
}
function urlTestChildDefaults() {
return {
check_interval: "3m",
tolerance: "50",
testing_url: "https://www.gstatic.com/generate_204",
idle_timeout: "30m",
interrupt_exist_connections: "1",
pin_dashboard: "1",
filter_mode: "disabled",
detect_server_country: "flag_emoji",
};
}
function priorityGroupSettingsKeys() {
return [
"name",
"health_url",
"active_check_interval",
"check_timeout",
"recovery_check_interval",
"pick_fastest",
"switch_to_faster_same_priority",
"fastest_check_interval",
"interrupt_exist_connections",
"pin_dashboard",
];
}
function defaultPriorityGroupSettings() {
return {
name: "",
health_url: "https://www.gstatic.com/generate_204",
active_check_interval: "5s",
check_timeout: "2s",
recovery_check_interval: "15s",
pick_fastest: "0",
switch_to_faster_same_priority: "0",
fastest_check_interval: "3m",
interrupt_exist_connections: "1",
pin_dashboard: "1",
};
}
function priorityGroupChildDefaults() {
return {
health_url: "https://www.gstatic.com/generate_204",
active_check_interval: "5s",
check_timeout: "2s",
recovery_check_interval: "15s",
pick_fastest: "0",
switch_to_faster_same_priority: "0",
fastest_check_interval: "3m",
interrupt_exist_connections: "1",
pin_dashboard: "1",
};
}
function priorityLevelSettingsKeys() {
return [
"name",
"order",
"direct",
"filter_mode",
"detect_server_country",
"country",
"server_name",
"regex",
"include_proxy_parameters",
"include_protocols",
"include_transports",
"include_securities",
"exclude_countries",
"exclude_outbounds",
"exclude_regex",
"exclude_proxy_parameters",
"exclude_protocols",
"exclude_transports",
"exclude_securities",
];
}
function defaultPriorityLevelSettings() {
return {
name: "",
order: "0",
direct: "0",
filter_mode: "include",
detect_server_country: "flag_emoji",
};
}
function randomPriorityGroupId() {
for (let i = 0; i < 100; i += 1) {
const value = Math.floor(Math.random() * 0xffffffff)
.toString(16)
.padStart(8, "0");
const id = `pg_${value}`;
if (!uci.get(UCI_PACKAGE, id)) {
return id;
}
}
return `pg_${Date.now().toString(16)}`;
}
function addSubscriptionUrlItemOptions(itemSection, options = {}) {
const parentSectionForItem =
typeof options.parentSectionId === "function"
? options.parentSectionId
: parentSectionIdForItem;
let o = itemSection.option(
form.Flag,
"subscription_update_enabled",
_("Subscription auto update"),
_("Update this subscription automatically"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Value,
"subscription_update_interval",
_("Subscription update interval"),
_("Use sing-box duration format like 1d, 12h or 30m"),
);
o.depends("subscription_update_enabled", "1");
o.placeholder = "1h";
o.validate = function (itemId, value) {
return optionMapValue(this, itemId, "subscription_update_enabled") === "1"
? validateRequiredSingBoxDuration(value)
: validateOptionalSingBoxDuration(value);
};
o = itemSection.option(
form.Flag,
"download_via_proxy_enabled",
_("Download subscription through a section"),
_("Download subscriptions via the selected section"),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(
form.ListValue,
"download_via_proxy_section",
_("Download through"),
);
o.depends("download_via_proxy_enabled", "1");
o.load = function (itemId) {
const sectionId = parentSectionForItem(itemId);
refreshOptionChoices(this, subscriptionDownloadTargetChoices(sectionId));
return optionMapValue(this, itemId, "download_via_proxy_section") || "";
};
o.validate = function (itemId, value) {
const sectionId = parentSectionForItem(itemId);
if (optionMapValue(this, itemId, "download_via_proxy_enabled") !== "1") {
return true;
}
if (!value) {
return _("Select a section for downloading this subscription");
}
return true;
};
o = itemSection.option(
form.Flag,
"auto_user_agent",
_("Automatic User-Agent selection"),
_(
"Try compatible User-Agent profiles automatically when downloading this subscription",
),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Value,
"user_agent",
_("User-Agent"),
_("Select a common client profile or enter a custom User-Agent"),
);
o.depends("auto_user_agent", "0");
o.placeholder = _("-- Select --");
o.rmempty = false;
subscriptionUserAgentChoices().forEach((choice) => o.value(choice));
o.load = function (itemId) {
return optionMapValue(this, itemId, "user_agent") || "";
};
o.validate = function (itemId, value) {
if (optionMapValue(this, itemId, "auto_user_agent") !== "0") {
return true;
}
return `${value || ""}`.trim() ? true : _("Select or enter a User-Agent");
};
o = itemSection.option(
form.Flag,
"auto_hwid",
_("Auto-generate HWID"),
_("Generate HWID from router hardware information for this subscription"),
);
o.default = "1";
o.rmempty = false;
const hwidOption = itemSection.option(
form.Value,
"hwid",
_("HWID"),
_("Enter the HWID sent with subscription requests"),
);
hwidOption.depends("auto_hwid", "0");
hwidOption.rmempty = false;
hwidOption.validate = function (itemId, value) {
if (optionMapValue(this, itemId, "auto_hwid") !== "0") {
return true;
}
return `${value || ""}`.trim() ? true : _("Enter HWID");
};
o = itemSection.option(
form.Button,
"_generate_hwid",
_("Generate HWID"),
_("Generate a random 16-character hexadecimal HWID"),
);
o.depends("auto_hwid", "0");
o.inputstyle = "action";
o.inputtitle = _("Generate HWID");
o.onclick = function (_event, itemId) {
hwidOption.getUIElement(itemId).setValue(generateHwid16());
};
o = itemSection.option(
form.Flag,
"custom_device_headers",
_("Custom device headers"),
_("Override the default OpenWrt device headers for this subscription"),
);
o.default = "0";
o.rmempty = false;
[
["device_os", "X-Device-OS"],
["ver_os", "X-Ver-OS"],
["device_model", "X-Device-Model"],
["device_locale", "X-Device-Locale"],
["app_version", "X-App-Version"],
["accept_language", "Accept-Language"],
].forEach(([key, label]) => {
o = itemSection.option(
form.Value,
key,
label,
_("Leave empty to omit this header"),
);
o.depends("custom_device_headers", "1");
o.rmempty = true;
o.retain = true;
});
o = itemSection.option(
form.Flag,
"show_dashboard_metadata",
_("Show metadata on dashboard"),
_("Show subscription metadata for this source on the dashboard"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"prefix_nodes",
_("Add prefix to nodes"),
_(
"Automatically add text to the name of each server from this subscription for convenient filtering.",
),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(form.Value, "node_prefix", _("Prefix text"));
o.depends("prefix_nodes", "1");
o.rmempty = false;
o = itemSection.option(
form.Flag,
"include_urltest_groups",
_("Import subscription URLTest groups"),
_("Import URLTest groups returned by this subscription provider"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"hide_urltest_group_outbounds",
_("Hide URLTest group nodes"),
_(
"Hide individual nodes that are already included in imported subscription URLTest groups",
),
);
o.depends("include_urltest_groups", "1");
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"hide_detour_outbounds",
_("Hide cascade connection nodes"),
_("Hide intermediate nodes used as detours by other subscription nodes"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"subscription_insecure",
_("Allow insecure TLS"),
_(
"Download this subscription without TLS certificate verification (for panels with self-signed or mismatched certificates)",
),
);
o.default = "0";
o.rmempty = false;
}
function addInterfaceItemOptions(itemSection) {
let o = itemSection.option(
form.Flag,
"domain_resolver_enabled",
_("Domain Resolver"),
_("Enable built-in DNS resolver for domains handled by this section"),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(
form.ListValue,
"domain_resolver_dns_type",
_("DNS protocol"),
_("DNS protocol used by the resolver"),
);
o.depends("domain_resolver_enabled", "1");
dnsTypeChoices().forEach((choice) => o.value(choice.value, choice.label));
o.default = "udp";
o = itemSection.option(
form.Value,
"domain_resolver_dns_server",
_("DNS server"),
_("DNS server used by the resolver"),
);
o.depends("domain_resolver_enabled", "1");
o.default = "8.8.8.8";
o.validate = function (itemId, value) {
if (optionMapValue(this, itemId, "domain_resolver_enabled") !== "1") {
return true;
}
const validation = main.validateDNS(value);
return validation.valid ? true : validation.message;
};
}
function addUrlTestItemOptions(itemSection, options = {}) {
const parentSectionForItem =
typeof options.parentSectionId === "function"
? options.parentSectionId
: parentSectionIdForItem;
let o = itemSection.option(
form.Value,
"name",
_("Display name"),
_("Name displayed on the dashboard"),
);
o.rmempty = false;
o.load = function (itemId) {
return (
optionMapValue(this, itemId, "name") ||
optionMapValue(this, itemId, "display_name") ||
""
);
};
o.validate = function (_itemId, value) {
return `${value || ""}`.trim() ? true : _("Enter a display name");
};
o = itemSection.option(
form.Value,
"check_interval",
_("Check interval"),
_("Use sing-box duration format like 1d, 12h or 30m"),
);
o.default = "3m";
o.rmempty = false;
o.validate = function (_itemId, value) {
return validateRequiredSingBoxDuration(value);
};
o = itemSection.option(
form.Value,
"tolerance",
_("Tolerance"),
_(
"Minimum latency difference in milliseconds that triggers switching to a faster server.",
),
);
o.default = "50";
o.rmempty = false;
o.validate = function (_itemId, value) {
return validateUrlTestTolerance(value);
};
o = itemSection.option(
form.Value,
"testing_url",
_("Check URL"),
_("URL used to test server latency"),
);
o.default = "https://www.gstatic.com/generate_204";
o.rmempty = false;
urlTestUrlChoices().forEach((value) => o.value(value));
o.validate = function (_itemId, value) {
return validateUrlTestUrl(value);
};
o = itemSection.option(
form.Value,
"idle_timeout",
_("Idle timeout"),
_(
"Stop checking when URLTest group is not used. Use sing-box duration format like 1d, 12h or 30m.",
),
);
o.default = "30m";
o.rmempty = false;
o.validate = function (_itemId, value) {
return validateRequiredSingBoxDuration(value);
};
o = itemSection.option(
form.Flag,
"interrupt_exist_connections",
_("Interrupt connections"),
_("Interrupt connections when URLTest switches the selected server"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"pin_dashboard",
_("Pin on dashboard"),
_("Keep URLTest before latency-sorted servers"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.ListValue,
"filter_mode",
_("Server filtering"),
_("Allows limiting the list of servers for URLTest"),
);
urlTestFilterModeChoices().forEach((choice) =>
o.value(choice.value, choice.label),
);
o.default = "disabled";
o = itemSection.option(
form.ListValue,
"detect_server_country",
_("Detect server country"),
);
o.depends("filter_mode", "exclude");
o.depends("filter_mode", "include");
o.depends("filter_mode", "mixed");
serverCountryDetectionChoices().forEach((choice) =>
o.value(choice.value, choice.label),
);
o.default = "flag_emoji";
const includeProxyParameterOptions = {
prefix: "include",
toggleLabel: _("Include by proxy parameters"),
toggleDescription: _(
"Additionally filter servers by protocol, transport, and security. Add only servers matching the specified parameters.",
),
dependencies: [{ filter_mode: "include" }, { filter_mode: "mixed" }],
protocolDescription: _(
"Test only servers with one of the selected protocols.",
),
transportDescription: _(
"Test only servers with one of the selected transports.",
),
securityDescription: _(
"Test only servers with one of the selected security types.",
),
};
const excludeProxyParameterOptions = {
prefix: "exclude",
toggleLabel: _("Exclude by proxy parameters"),
toggleDescription: _(
"Additionally exclude servers by protocol, transport, and security. Exclude only servers matching the specified parameters.",
),
dependencies: [{ filter_mode: "exclude" }, { filter_mode: "mixed" }],
protocolDescription: _(
"Do not test servers with one of the selected protocols.",
),
transportDescription: _(
"Do not test servers with one of the selected transports.",
),
securityDescription: _(
"Do not test servers with one of the selected security types.",
),
};
[
[
"include_countries",
_("Include countries"),
_("Test servers only from the specified countries."),
countryChoices(),
validateCountryCode,
["include", "mixed"],
],
[
"include_outbounds",
_("Include servers"),
_("Test only selected servers."),
null,
null,
["include", "mixed"],
],
[
"include_regex",
_("Include by regular expression"),
_("Test servers whose names match the expression."),
null,
validateRegex,
["include", "mixed"],
],
[
"exclude_countries",
_("Exclude countries"),
_("Do not test servers from these countries."),
countryChoices(),
validateCountryCode,
["exclude", "mixed"],
],
[
"exclude_outbounds",
_("Exclude servers"),
_("Do not test specified servers."),
null,
null,
["exclude", "mixed"],
],
[
"exclude_regex",
_("Exclude by regular expression"),
_("Do not test servers whose names match the expression."),
null,
validateRegex,
["exclude", "mixed"],
],
].forEach(([key, label, description, choices, validator, modes]) => {
const list = itemSection.option(form.DynamicList, key, label, description);
modes.forEach((mode) => list.depends("filter_mode", mode));
list.rmempty = true;
if (choices) {
choices.forEach((choice) => list.value(choice.value, choice.label));
list.placeholder = _("-- Select --");
}
if (key.endsWith("_outbounds")) {
list.load = function (itemId) {
const sectionId = parentSectionForItem(itemId) || "Main";
const values = normalizeOptionValues(optionMapValue(this, itemId, key));
return loadOutboundNameChoices(sectionId).then(() => {
refreshOptionChoices(
this,
currentOutboundNameChoices(sectionId, values),
);
return values;
});
};
list.placeholder = _("-- Select --");
configureLiveDynamicListChoices(list, (itemId, values) =>
currentOutboundNameChoices(
parentSectionForItem(itemId) || "Main",
values,
),
);
}
if (validator) {
list.validate = function (_itemId, value) {
return validator(null, value);
};
}
if (key === "include_regex") {
addProxyParameterFilterOptions(itemSection, includeProxyParameterOptions);
} else if (key === "exclude_regex") {
addProxyParameterFilterOptions(itemSection, excludeProxyParameterOptions);
}
});
}
function priorityLevelSettingsForValidation(groupId, levelId, option) {
const store = childPendingSettingsStore(option, groupId);
if (isExistingChildItem(groupId, levelId, "priority_level", "group")) {
return readChildSettings(
levelId,
priorityLevelSettingsKeys(),
defaultPriorityLevelSettings(),
);
}
return Object.assign({}, store[levelId] || {});
}
function validatePriorityLevelItemsBeforeSave(groupId, values, option) {
const normalizedValues = normalizeDynamicListItems(values);
for (const value of normalizedValues) {
const levelId = `${value || ""}`;
const settings = priorityLevelSettingsForValidation(
groupId,
levelId,
option,
);
if (!`${settings.name || ""}`.trim()) {
return _("Enter a level name");
}
}
return true;
}
function addPriorityLevelItemOptions(itemSection, options = {}) {
const parentSectionForItem =
typeof options.parentSectionId === "function"
? options.parentSectionId
: parentSectionIdForItem;
let o = itemSection.option(
form.Value,
"name",
_("Level name"),
_("Name shown in the priority level list"),
);
o.rmempty = false;
o.validate = function (_itemId, value) {
return `${value || ""}`.trim() ? true : _("Enter a level name");
};
o = itemSection.option(
form.Flag,
"direct",
_("Direct connection"),
_("Traffic for this level goes directly."),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(
form.ListValue,
"filter_mode",
_("Server filtering"),
_(
"All remaining servers means every server not already assigned to a higher-priority level.",
),
);
priorityLevelFilterModeChoices().forEach((choice) =>
o.value(choice.value, choice.label),
);
o.default = "include";
o.depends("direct", "0");
o = itemSection.option(
form.ListValue,
"detect_server_country",
_("Detect server country"),
);
["exclude", "include", "mixed"].forEach((mode) =>
o.depends({ direct: "0", filter_mode: mode }),
);
serverCountryDetectionChoices().forEach((choice) =>
o.value(choice.value, choice.label),
);
o.default = "flag_emoji";
const includeProxyParameterOptions = {
prefix: "include",
toggleLabel: _("Include by proxy parameters"),
toggleDescription: _(
"Additionally filter servers by protocol, transport, and security. Add only servers matching the specified parameters.",
),
dependencies: [
{ direct: "0", filter_mode: "include" },
{ direct: "0", filter_mode: "mixed" },
],
protocolDescription: _("Only servers with one of the selected protocols."),
transportDescription: _(
"Only servers with one of the selected transports.",
),
securityDescription: _(
"Only servers with one of the selected security types.",
),
};
const excludeProxyParameterOptions = {
prefix: "exclude",
toggleLabel: _("Exclude by proxy parameters"),
toggleDescription: _(
"Additionally exclude servers by protocol, transport, and security. Exclude only servers matching the specified parameters.",
),
dependencies: [
{ direct: "0", filter_mode: "exclude" },
{ direct: "0", filter_mode: "mixed" },
],
protocolDescription: _(
"Exclude servers with one of the selected protocols from this level.",
),
transportDescription: _(
"Exclude servers with one of the selected transports from this level.",
),
securityDescription: _(
"Exclude servers with one of the selected security types from this level.",
),
};
[
[
"country",
_("Include countries"),
_("Only from the specified countries."),
countryChoices(),
validateCountryCode,
["include", "mixed"],
],
[
"server_name",
_("Include servers"),
_("Only the specified servers."),
null,
null,
["include", "mixed"],
],
[
"regex",
_("Include by regular expression"),
_("Only servers whose names match the expression."),
null,
validateRegex,
["include", "mixed"],
],
[
"exclude_countries",
_("Exclude countries"),
_("Remove servers from the specified countries from this level."),
countryChoices(),
validateCountryCode,
["exclude", "mixed"],
],
[
"exclude_outbounds",
_("Exclude servers"),
_("Remove the specified servers from this level."),
null,
null,
["exclude", "mixed"],
],
[
"exclude_regex",
_("Exclude by regular expression"),
_("Remove servers whose names match the expression from this level."),
null,
validateRegex,
["exclude", "mixed"],
],
].forEach(([key, label, description, choices, validator, modes]) => {
const list = itemSection.option(form.DynamicList, key, label, description);
modes.forEach((mode) => list.depends({ direct: "0", filter_mode: mode }));
list.rmempty = true;
if (choices) {
choices.forEach((choice) => list.value(choice.value, choice.label));
list.placeholder = _("-- Select --");
}
if (key === "server_name" || key.endsWith("_outbounds")) {
list.load = function (itemId) {
const sectionId = parentSectionForItem(itemId) || "Main";
const values = normalizeOptionValues(optionMapValue(this, itemId, key));
return loadOutboundNameChoices(sectionId).then(() => {
refreshOptionChoices(
this,
currentOutboundNameChoices(sectionId, values),
);
return values;
});
};
list.placeholder = _("-- Select --");
configureLiveDynamicListChoices(list, (itemId, values) =>
currentOutboundNameChoices(
parentSectionForItem(itemId) || "Main",
values,
),
);
}
if (validator) {
list.validate = function (_itemId, value) {
return validator(null, value);
};
}
if (key === "regex") {
addProxyParameterFilterOptions(itemSection, includeProxyParameterOptions);
} else if (key === "exclude_regex") {
addProxyParameterFilterOptions(itemSection, excludeProxyParameterOptions);
}
});
}
function addPriorityGroupItemOptions(itemSection, options = {}) {
const parentSectionForGroup =
typeof options.parentSectionId === "function"
? options.parentSectionId
: parentSectionIdForItem;
const ownerId =
typeof options.ownerId === "function" ? options.ownerId : () => "";
let o = itemSection.option(
form.Value,
"name",
_("Display name"),
_("Name displayed on the dashboard"),
);
o.rmempty = false;
o.validate = function (_itemId, value) {
return `${value || ""}`.trim() ? true : _("Enter a display name");
};
o = itemSection.option(
form.Value,
"health_url",
_("Check URL"),
_("URL used to check whether a server is alive"),
);
o.default = "https://www.gstatic.com/generate_204";
o.rmempty = false;
urlTestUrlChoices().forEach((value) => o.value(value));
o.validate = function (_itemId, value) {
return validateUrlTestUrl(value);
};
o = itemSection.option(
form.Value,
"active_check_interval",
_("Check interval"),
_("How often the currently selected server is checked"),
);
o.default = "5s";
o.rmempty = false;
o.validate = function (_itemId, value) {
return validateRequiredSingBoxDuration(value);
};
o = itemSection.option(
form.Value,
"check_timeout",
_("Unavailability timeout"),
_("Check timeout after which the server is considered dead"),
);
o.default = "2s";
o.rmempty = false;
o.validate = function (_itemId, value) {
return validateRequiredSingBoxDuration(value);
};
o = itemSection.option(
form.Value,
"recovery_check_interval",
_("Higher-level check interval"),
_(
"How often higher priority levels are checked while a lower level is active",
),
);
o.default = "15s";
o.rmempty = false;
o.validate = function (_itemId, value) {
return validateRequiredSingBoxDuration(value);
};
o = itemSection.option(
form.Flag,
"pick_fastest",
_("Select the fastest node"),
_(
"When switching to another level, test every server and select the fastest instead of the first working one.",
),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"switch_to_faster_same_priority",
_("Automatically select the fastest node in the current level"),
_(
"Periodically check the current level and switch to a faster server even when the current one works.",
),
);
o.default = "0";
o.rmempty = false;
o = itemSection.option(
form.Value,
"fastest_check_interval",
_("Faster server search interval"),
_("Use sing-box duration format like 1d, 12h or 30m"),
);
o.depends("switch_to_faster_same_priority", "1");
o.default = "3m";
o.rmempty = false;
o.validate = function (itemId, value) {
return optionMapValue(this, itemId, "switch_to_faster_same_priority") ===
"1"
? validateRequiredSingBoxDuration(value)
: true;
};
o = itemSection.option(
form.Flag,
"interrupt_exist_connections",
_("Interrupt connections"),
_("Interrupt connections when priority failover switches server"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
form.Flag,
"pin_dashboard",
_("Pin on dashboard"),
_("Keep Priority before latency-sorted servers"),
);
o.default = "1";
o.rmempty = false;
o = itemSection.option(
ButtonAddSettingsDynamicList,
"priority_level",
_("Priority levels"),
_("Top level has the highest priority; lower levels are used as fallback"),
);
o.rmempty = true;
o.modalonly = true;
o.addButtonLabel = _("+ Add level");
o.childType = "priority_level";
o.ownerOption = "group";
o.childOwnerId = ownerId;
o.parentSectionId = parentSectionForGroup;
o.childValueOption = "name";
o.childDefaults = defaultPriorityLevelSettings();
o.renderItemSettingsModal = showPriorityLevelSettingsModal;
o.validateItemsOnSave = function (groupId, values) {
return validatePriorityLevelItemsBeforeSave(groupId, values, this);
};
o.hasItemSettings = function (groupId, value) {
const normalized = `${value || ""}`.trim();
if (isExistingChildItem(groupId, normalized, "priority_level", "group")) {
return true;
}
return normalized.length > 0;
};
o.inputValueForItem = function (groupId, value) {
const inputValue = childItemInputValue(
groupId,
value,
"priority_level",
"name",
"group",
);
const store = childPendingSettingsStore(this, groupId);
return store[inputValue] && store[inputValue].name
? store[inputValue].name
: inputValue;
};
o.stagedChildSettings = function (groupId, value) {
const id = childItemInputValue(
groupId,
value,
"priority_level",
"name",
"group",
);
const store = childPendingSettingsStore(this, groupId);
return store[id] ? Object.assign({}, store[id]) : null;
};
o.clearStagedChildSettings = function (groupId) {
if (this.pendingChildSettings) {
delete this.pendingChildSettings[groupId];
}
};
o.afterMaterializeChildItems = function (_groupId, itemIds) {
itemIds.forEach((itemId, index) => {
uci.set(UCI_PACKAGE, itemId, "order", `${index}`);
});
};
o.renderListItemLabel = function (groupId, itemId) {
return E(
"span",
{ class: "fkp-dynlist-label" },
this.inputValueForItem(groupId, itemId),
);
};
}
function addDashboardGroupFilterOption(
optionSection,
key,
label,
description,
modes,
) {
const list = optionSection.option(form.DynamicList, key, label, description);
modes.forEach((mode) => {
list.depends({
action: "connection",
dashboard_filter_mode: mode,
urltest: /.+/,
});
list.depends({
action: "connection",
dashboard_filter_mode: mode,
priority_group: /.+/,
});
});
list.rmempty = true;
list.placeholder = _("-- Select --");
list.load = function (section_id) {
const values = getConfigListValues(section_id, key);
const choices = currentSectionGroupChoices(section_id, values);
refreshOptionChoices(this, choices);
return values;
};
list.validate = function (section_id, value) {
const selected = new Set(
currentSectionGroupChoices(section_id).map((choice) => choice.value),
);
return normalizeDynamicListItems(value).every((item) => selected.has(item))
? true
: _("Select an existing URLTest or Priority group");
};
configureLiveDynamicListChoices(list, currentSectionGroupChoices);
}
function addDashboardServerFilterOptions(section) {
const optionSection = {
option: (optionType, ...args) => {
const option = section.taboption("settings", optionType, ...args);
option.modalonly = true;
return option;
},
};
let o = optionSection.option(
form.ListValue,
"dashboard_filter_mode",
_("Servers on dashboard"),
_("Filter the servers that will be displayed on the dashboard."),
);
urlTestFilterModeChoices().forEach((choice) =>
o.value(choice.value, choice.label),
);
o.default = "disabled";
o.depends("action", "connection");
o = optionSection.option(
form.ListValue,
"dashboard_detect_server_country",
_("Detect server country"),
);
["exclude", "include", "mixed"].forEach((mode) =>
o.depends({ action: "connection", dashboard_filter_mode: mode }),
);
serverCountryDetectionChoices().forEach((choice) =>
o.value(choice.value, choice.label),
);
o.default = "flag_emoji";
const includeProxyParameterOptions = {
prefix: "dashboard_include",
toggleLabel: _("Include by proxy parameters"),
toggleDescription: _(
"Additionally show servers by protocol, transport, and security. Add only servers matching the specified parameters.",
),
dependencies: [
{ action: "connection", dashboard_filter_mode: "include" },
{ action: "connection", dashboard_filter_mode: "mixed" },
],
protocolDescription: _(
"Show only servers with one of the selected protocols.",
),
transportDescription: _(
"Show only servers with one of the selected transports.",
),
securityDescription: _(
"Show only servers with one of the selected security types.",
),
};
const excludeProxyParameterOptions = {
prefix: "dashboard_exclude",
toggleLabel: _("Exclude by proxy parameters"),
toggleDescription: _(
"Additionally hide servers by protocol, transport, and security. Hide servers matching the specified parameters.",
),
dependencies: [
{ action: "connection", dashboard_filter_mode: "exclude" },
{ action: "connection", dashboard_filter_mode: "mixed" },
],
protocolDescription: _("Hide servers with one of the selected protocols."),
transportDescription: _(
"Hide servers with one of the selected transports.",
),
securityDescription: _(
"Hide servers with one of the selected security types.",
),
};
[
[
"dashboard_include_countries",
_("Include countries"),
_("Show servers only from the specified countries."),
countryChoices(),
validateCountryCode,
["include", "mixed"],
],
[
"dashboard_include_outbounds",
_("Include servers"),
_("Show only selected servers."),
null,
null,
["include", "mixed"],
],
[
"dashboard_include_regex",
_("Include by regular expression"),
_("Show servers whose names match the expression."),
null,
validateRegex,
["include", "mixed"],
],
[
"dashboard_exclude_countries",
_("Exclude countries"),
_("Hide servers from the specified countries."),
countryChoices(),
validateCountryCode,
["exclude", "mixed"],
],
[
"dashboard_exclude_outbounds",
_("Exclude servers"),
_("Hide selected servers."),
null,
null,
["exclude", "mixed"],
],
[
"dashboard_exclude_regex",
_("Exclude by regular expression"),
_("Hide servers whose names match the expression."),
null,
validateRegex,
["exclude", "mixed"],
],
].forEach(([key, label, description, choices, validator, modes]) => {
const list = optionSection.option(
form.DynamicList,
key,
label,
description,
);
modes.forEach((mode) =>
list.depends({ action: "connection", dashboard_filter_mode: mode }),
);
list.rmempty = true;
if (choices) {
choices.forEach((choice) => list.value(choice.value, choice.label));
list.placeholder = _("-- Select --");
}
if (key.endsWith("_outbounds")) {
list.load = function (section_id) {
const values = getConfigListValues(section_id, key);
loadOutboundNameChoices(section_id).then(() => {
refreshDashboardFilterChoiceWidgets(section_id);
});
return values;
};
list.placeholder = _("-- Select --");
configureLiveDynamicListChoices(list, currentOutboundNameChoices);
}
if (validator) {
list.validate = function (_section_id, value) {
return validator(null, value);
};
}
if (key === "dashboard_include_regex") {
addDashboardGroupFilterOption(
optionSection,
"dashboard_include_groups",
_("Add URLTest / Priority group"),
_(
"Show servers from selected groups without repeating their filtering criteria.",
),
["include", "mixed"],
);
addProxyParameterFilterOptions(
optionSection,
includeProxyParameterOptions,
);
} else if (key === "dashboard_exclude_regex") {
addDashboardGroupFilterOption(
optionSection,
"dashboard_exclude_groups",
_("Exclude URLTest / Priority group"),
_(
"Hide servers from selected groups without repeating their filtering criteria.",
),
["exclude", "mixed"],
);
addProxyParameterFilterOptions(
optionSection,
excludeProxyParameterOptions,
);
}
});
}
function settingValueEquals(left, right) {
const normalize = (value) => {
if (Array.isArray(value)) {
return JSON.stringify(value.map((item) => `${item || ""}`));
}
return value === undefined || value === null ? "" : `${value}`;
};
return normalize(left) === normalize(right);
}
function readChildSettings(itemId, keys, defaults) {
const result = Object.assign({}, defaults || {});
keys.forEach((key) => {
const value = uci.get(UCI_PACKAGE, itemId, key);
if (value !== null && value !== undefined) {
result[key] = Array.isArray(value) ? value.slice() : value;
}
});
return result;
}
function changedSettings(base, next, keys) {
const result = {};
keys.forEach((key) => {
if (!settingValueEquals(base ? base[key] : null, next ? next[key] : null)) {
result[key] = next ? next[key] : null;
}
});
return result;
}
function hasChangedSettings(settings) {
return Object.keys(settings || {}).length > 0;
}
function childPendingSettingsStore(option, section_id) {
if (!option.pendingChildSettings) {
option.pendingChildSettings = {};
}
if (!option.pendingChildSettings[section_id]) {
option.pendingChildSettings[section_id] = {};
}
return option.pendingChildSettings[section_id];
}
function pendingChildSettings(option, section_id, value, defaults) {
value = `${value || ""}`.trim();
const store = childPendingSettingsStore(option, section_id);
if (!store[value]) {
store[value] = Object.assign({}, defaults || {});
}
return store[value];
}
function renderStackedJsonSettingsModal(title, map, onSave) {
const modal = document.querySelector("#modal_overlay > .modal.cbi-modal");
const activeMap = modal ? modal.querySelector(".cbi-map:not(.hidden)") : null;
const buttonRow = modal ? modal.querySelector("div.button-row") : null;
const heading = modal ? modal.querySelector("h4") : null;
if (!modal || !activeMap || !buttonRow || !heading) {
return Promise.resolve();
}
return map
.load()
.then(() => map.render())
.then((nodes) => {
const titleNode = E("span", title ? ` » ${title}` : "");
const originalButtonClass = buttonRow.getAttribute("class") || "";
const originalButtonNodes = Array.from(buttonRow.childNodes);
let closed = false;
let saveButton;
let validationSummary;
const clearValidationSummary = () => {
if (validationSummary && validationSummary.parentNode) {
validationSummary.parentNode.removeChild(validationSummary);
}
validationSummary = null;
};
const showValidationSummary = (error) => {
clearValidationSummary();
const message = error?.message || "";
validationSummary = E(
"div",
{
class:
"alert-message warning fkp-stacked-settings-validation-summary",
},
[
E("strong", {}, _("Cannot save settings")),
E("div", {}, _("Fix the highlighted fields and save again.")),
message ? E("small", {}, message) : "",
],
);
buttonRow.parentNode.insertBefore(validationSummary, buttonRow);
const invalidInput = nodes.querySelector(".cbi-input-invalid");
if (invalidInput) {
invalidInput.scrollIntoView({ block: "center", behavior: "smooth" });
invalidInput.focus({ preventScroll: true });
}
};
const restoreButtonRow = () => {
buttonRow.textContent = "";
originalButtonNodes.forEach((node) => buttonRow.appendChild(node));
buttonRow.setAttribute("class", originalButtonClass);
};
const close = () => {
if (closed) {
return;
}
closed = true;
clearValidationSummary();
if (nodes.parentNode) {
nodes.parentNode.removeChild(nodes);
}
if (titleNode.parentNode) {
titleNode.parentNode.removeChild(titleNode);
}
activeMap.classList.remove("hidden");
restoreButtonRow();
};
const save = () => {
if (saveButton) {
saveButton.disabled = true;
}
clearValidationSummary();
return map
.parse()
.then(() => {
onSave(cleanFormSectionData(map.data.get(map.config, "settings")));
close();
})
.catch((error) => {
if (saveButton) {
saveButton.disabled = false;
}
showValidationSummary(error);
});
};
buttonRow.textContent = "";
buttonRow.append(
E(
"button",
{
class: "btn cbi-button",
click: close,
},
_("Close"),
),
" ",
(saveButton = E(
"button",
{
class: "btn cbi-button cbi-button-positive important",
click: save,
},
_("Save"),
)),
);
heading.appendChild(titleNode);
activeMap.classList.add("hidden");
activeMap.parentNode.insertBefore(nodes, activeMap.nextElementSibling);
});
}
function showChildItemSettingsModal(section_id, itemValue, option, settings) {
const value = `${itemValue || ""}`.trim();
const existing = isExistingChildItem(
section_id,
value,
settings.typeName,
settings.ownerOption,
);
const inputValue = childItemInputValue(
section_id,
value,
settings.typeName,
settings.valueOption,
settings.ownerOption,
);
const defaults =
typeof settings.defaults === "function"
? settings.defaults(inputValue)
: Object.assign({}, settings.defaults || {});
const initialSettings = existing
? readChildSettings(value, settings.keys, defaults)
: Object.assign(
{},
pendingChildSettings(option, section_id, inputValue, defaults),
);
const data = {
settings: Object.assign({}, initialSettings),
};
const map = new form.JSONMap(data);
const itemSection = map.section(form.NamedSection, "settings");
itemSection.anonymous = true;
itemSection.addremove = false;
settings.addOptions(itemSection, {
parentSectionId: () => section_id,
ownerId: () => value || section_id,
});
return renderStackedJsonSettingsModal(
settings.title(inputValue),
map,
(nextSettings) => {
if (existing) {
const diff = changedSettings(
initialSettings,
nextSettings,
settings.keys,
);
if (hasChangedSettings(diff)) {
applyChildItemSettings(value, diff);
}
if (typeof settings.afterSave === "function") {
settings.afterSave(value, inputValue, nextSettings, existing);
}
return;
}
const nextInputValue =
`${nextSettings[settings.valueOption] || inputValue || ""}`.trim();
const store = childPendingSettingsStore(option, section_id);
if (nextInputValue !== inputValue) {
delete store[inputValue];
}
store[nextInputValue] = nextSettings;
if (typeof settings.afterSave === "function") {
settings.afterSave(value, nextInputValue, nextSettings, existing);
}
},
);
}
function ruleSetIncludesSubnets(section_id, value) {
const settings = readItemSettingsMap(section_id, RULE_SET_ITEM_SETTINGS_KEY);
const itemSettings = settings && settings[value];
if (
itemSettings &&
typeof itemSettings === "object" &&
itemSettings.include_subnets != null
) {
return itemSettingsFlag(itemSettings, "include_subnets", false);
}
return getConfigListValues(section_id, "rule_set_with_subnets").includes(
value,
);
}
function showSubscriptionUrlSettingsModal(_section_id, itemValue, option) {
return showChildItemSettingsModal(_section_id, itemValue, option, {
typeName: "subscription_url",
valueOption: "url",
keys: subscriptionUrlSettingsKeys(),
defaults: defaultSubscriptionUrlSettings(),
addOptions: addSubscriptionUrlItemOptions,
title: () => _("Subscription URL settings"),
});
}
function showInterfaceSettingsModal(_section_id, itemValue, option) {
return showChildItemSettingsModal(_section_id, itemValue, option, {
typeName: "section_interface",
valueOption: "name",
keys: interfaceSettingsKeys(),
defaults: defaultInterfaceSettings(),
addOptions: addInterfaceItemOptions,
title: () => _("Network interface settings"),
});
}
function showUrlTestSettingsModal(
_section_id,
itemValue,
option,
widget,
itemNode,
context = {},
) {
const sectionId = _section_id || "Main";
// Pre-warm the cache BEFORE opening the modal.
// CBINamedSection.render() calls renderUCISection() directly without
// calling section.load() first, so list.load() (which would populate
// the outbound name cache) is never awaited before renderWidget().
// By loading the cache here the dropdown has choices on first render.
loadOutboundNameChoices(sectionId).then(() => {
showChildItemSettingsModal(_section_id, itemValue, option, {
typeName: "urltest",
valueOption: "name",
keys: urlTestSettingsKeys(),
defaults: defaultUrlTestSettings,
addOptions: addUrlTestItemOptions,
title: (name) => {
const normalized = `${name || ""}`.trim();
return !normalized || /^urltest-[a-z0-9]+-\d+$/.test(normalized)
? _("URLTest settings")
: `${_("URLTest settings")}: ${normalized}`;
},
afterSave: (itemId, inputValue, settings, existing) => {
const displayName = `${settings.name || inputValue || ""}`.trim();
if (existing) {
uci.unset(UCI_PACKAGE, itemId, "id");
uci.unset(UCI_PACKAGE, itemId, "display_name");
updateDynamicListItemLabel(itemNode, displayName);
return;
}
if (context.adding) {
addDynamicListItem(widget, displayName, displayName);
} else {
updateDynamicListItemLabel(itemNode, displayName);
}
},
});
});
}
function showPriorityLevelSettingsModal(
groupId,
itemValue,
option,
widget,
itemNode,
context = {},
) {
const sectionId = context.parentSectionId || groupId || "Main";
// Pre-warm the outbound name cache before opening the modal.
// CBINamedSection.render() never calls section.load(), so list.load()
// is never invoked before renderWidget() — cache would be empty otherwise.
loadOutboundNameChoices(sectionId).then(() => {
showChildItemSettingsModal(groupId, itemValue, option, {
typeName: "priority_level",
ownerOption: "group",
valueOption: "name",
keys: priorityLevelSettingsKeys(),
defaults: defaultPriorityLevelSettings(),
addOptions: (itemSection) =>
addPriorityLevelItemOptions(itemSection, {
parentSectionId: () => context.parentSectionId || "",
}),
title: (name) => {
const normalized = `${name || ""}`.trim();
return normalized
? `${_("Priority level settings")}: ${normalized}`
: _("Priority level settings");
},
afterSave: (itemId, inputValue, settings, existing) => {
const displayName = `${settings.name || inputValue || ""}`.trim();
if (existing) {
updateDynamicListItemLabel(itemNode, displayName);
return;
}
if (context.adding) {
addDynamicListItem(widget, displayName, displayName);
} else {
updateDynamicListItemLabel(itemNode, displayName);
}
},
});
});
}
function createPriorityGroupItem(section_id, groupId, settings) {
const created = uci.add(UCI_PACKAGE, "priority_group", groupId) || groupId;
uci.set(UCI_PACKAGE, created, "section", section_id);
Object.entries(priorityGroupChildDefaults()).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== "") {
uci.set(UCI_PACKAGE, created, key, `${value}`);
}
});
applyChildItemSettings(created, settings);
return created;
}
function showPriorityGroupSettingsModal(
section_id,
itemValue,
option,
widget,
itemNode,
context = {},
) {
const groupId = context.adding
? randomPriorityGroupId()
: `${itemValue || ""}`.trim();
if (!groupId) {
return null;
}
return showChildItemSettingsModal(section_id, groupId, option, {
typeName: "priority_group",
valueOption: "name",
keys: priorityGroupSettingsKeys(),
defaults: defaultPriorityGroupSettings(),
addOptions: addPriorityGroupItemOptions,
title: (name) => {
if (context.adding) {
return _("Priority settings");
}
const normalized = `${name || ""}`.trim();
return normalized
? `${_("Priority settings")}: ${normalized}`
: _("Priority settings");
},
afterSave: (itemId, inputValue, settings, existing) => {
const displayName = `${settings.name || inputValue || ""}`.trim();
if (!existing) {
const created = createPriorityGroupItem(section_id, groupId, settings);
const store = childPendingSettingsStore(option, section_id);
delete store[groupId];
delete store[inputValue];
delete store[displayName];
addDynamicListItem(widget, created, displayName);
return;
}
updateDynamicListItemLabel(itemNode, displayName);
},
});
}
function validateUrlTestItemsBeforeSave(section_id, values, option) {
const store = childPendingSettingsStore(option, section_id);
for (const value of normalizeDynamicListItems(values)) {
const itemId = `${value || ""}`;
let name = "";
if (isExistingChildItem(section_id, itemId, "urltest")) {
name =
uci.get(UCI_PACKAGE, itemId, "name") ||
uci.get(UCI_PACKAGE, itemId, "display_name") ||
"";
} else if (store[itemId]) {
name = store[itemId].name || "";
}
if (!`${name || ""}`.trim()) {
return _("Enter a display name");
}
}
return true;
}
function validatePriorityGroupItemsBeforeSave(section_id, values) {
for (const value of normalizeDynamicListItems(values)) {
const groupId = `${value || ""}`.trim();
if (!isExistingChildItem(section_id, groupId, "priority_group")) {
return _("Open priority settings and enter a display name");
}
if (!`${uci.get(UCI_PACKAGE, groupId, "name") || ""}`.trim()) {
return _("Enter a display name");
}
}
return true;
}
function showOutboundJsonSettingsModal(
_section_id,
itemValue,
_option,
widget,
itemNode,
context = {},
) {
const data = {
settings: {
outbound_json: `${itemValue || ""}`,
},
};
const map = new form.JSONMap(data);
const itemSection = map.section(form.NamedSection, "settings");
itemSection.anonymous = true;
itemSection.addremove = false;
const jsonOption = itemSection.option(
form.TextValue,
"outbound_json",
_("JSON outbound"),
_("Enter a complete sing-box outbound object"),
);
jsonOption.rows = 12;
jsonOption.wrap = "soft";
jsonOption.textarea = true;
jsonOption.modalonly = true;
jsonOption.rmempty = false;
jsonOption.validate = function (_itemId, value) {
const usedTags =
widget && widget.node
? Array.from(widget.node.querySelectorAll(".item"))
.filter((item) => item !== itemNode)
.map((item) =>
outboundJsonDisplayTag(dynamicListItemCurrentValue(item, "")),
)
.filter(Boolean)
: [];
const validation = main.validateOutboundJson(`${value || ""}`, usedTags);
return validation.valid ? true : validation.message;
};
configureTextareaOption(jsonOption);
return renderStackedJsonSettingsModal(
_("JSON outbound settings"),
map,
(settings) => {
const value = `${settings.outbound_json || ""}`.trim();
if (context.adding) {
addDynamicListItem(widget, value);
return;
}
setDynamicListItemValue(itemNode, value);
updateDynamicListItemLabel(itemNode, outboundJsonDisplayTag(value));
if (widget && widget.node) {
widget.dispatchCbiDynlistChange(widget.node, value);
}
},
);
}
function validateOutboundJsonItemsBeforeSave(_section_id, values) {
const items = Array.isArray(values) ? values : values ? [values] : [];
const tags = [];
for (const value of items) {
const validation = main.validateOutboundJson(`${value || ""}`, tags);
if (!validation.valid) {
const tag = outboundJsonDisplayTag(value);
return tag ? `${tag}: ${validation.message}` : validation.message;
}
const tag = outboundJsonDisplayTag(value);
tags.push(tag);
}
return true;
}
function showRuleSetSettingsModal(section_id, itemValue, option, widget) {
const data = {
settings: {
include_subnets: ruleSetIncludesSubnets(section_id, itemValue)
? "1"
: "0",
},
};
const map = new form.JSONMap(data);
const section = map.section(form.NamedSection, "settings");
section.anonymous = true;
section.addremove = false;
const includeSubnets = section.option(
form.Flag,
"include_subnets",
_("Include IP addresses and subnets"),
_("Subnets from the list will be extracted and added to nftables"),
);
includeSubnets.default = "0";
includeSubnets.rmempty = false;
return renderStackedJsonSettingsModal(
_("Rule set settings"),
map,
(settings) => {
const value = settings.include_subnets === "1";
const refs = uniqueDynamicListItems(
widget && typeof widget.getValue === "function"
? widget.getValue()
: getCustomRulesetReferences(section_id),
);
const subnets = new Set(
getConfigListValues(section_id, "rule_set_with_subnets").filter((ref) =>
refs.includes(ref),
),
);
if (value) {
subnets.add(itemValue);
} else {
subnets.delete(itemValue);
}
writeListOption(
section_id,
"rule_set",
refs.filter((ref) => !subnets.has(ref)),
);
writeListOption(section_id, "rule_set_with_subnets", [...subnets]);
if (option && typeof option.getUIElement === "function") {
option.getUIElement(section_id).setValue(refs);
}
},
);
}
function ensureActionProvidersAvailabilityLoaded() {
if (actionProvidersAvailabilityState.loaded) {
return Promise.resolve(actionProvidersAvailabilityState);
}
if (actionProvidersAvailabilityPromise) {
return actionProvidersAvailabilityPromise;
}
if (actionProvidersAvailabilityLoader) {
actionProvidersAvailabilityPromise = actionProvidersAvailabilityLoader()
.then((capabilities) => {
updateActionProvidersAvailabilityState({
zapretInstalled: Boolean(capabilities?.zapretInstalled),
zapret2Installed: Boolean(capabilities?.zapret2Installed),
byedpiInstalled: Boolean(capabilities?.byedpiInstalled),
singBoxExtended: Boolean(capabilities?.singBoxExtended),
});
return actionProvidersAvailabilityState;
})
.catch(() => {
actionProvidersAvailabilityLoader = null;
actionProvidersAvailabilityPromise = null;
return ensureActionProvidersAvailabilityLoaded();
})
.finally(() => {
actionProvidersAvailabilityPromise = null;
});
return actionProvidersAvailabilityPromise;
}
actionProvidersAvailabilityPromise = Promise.allSettled([
main.TachyonShellMethods.checkZapretRuntime(),
main.TachyonShellMethods.checkZapret2Runtime(),
main.TachyonShellMethods.checkByedpiRuntime(),
])
.then(([zapretResult, zapret2Result, byedpiResult]) => {
const zapret =
zapretResult && zapretResult.status === "fulfilled"
? zapretResult.value
: null;
const zapret2 =
zapret2Result && zapret2Result.status === "fulfilled"
? zapret2Result.value
: null;
const byedpi =
byedpiResult && byedpiResult.status === "fulfilled"
? byedpiResult.value
: null;
actionProvidersAvailabilityState.loaded = true;
actionProvidersAvailabilityState.zapretInstalled = Boolean(
zapret && zapret.success && zapret.data && zapret.data.zapret_installed,
);
actionProvidersAvailabilityState.zapret2Installed = Boolean(
zapret2 &&
zapret2.success &&
zapret2.data &&
zapret2.data.zapret2_installed,
);
actionProvidersAvailabilityState.byedpiInstalled = Boolean(
byedpi && byedpi.success && byedpi.data && byedpi.data.byedpi_installed,
);
return actionProvidersAvailabilityState;
})
.catch(() => {
actionProvidersAvailabilityState.loaded = true;
actionProvidersAvailabilityState.zapretInstalled = false;
actionProvidersAvailabilityState.zapret2Installed = false;
actionProvidersAvailabilityState.byedpiInstalled = false;
// singBoxExtended is set via systemInfo, keep existing value
return actionProvidersAvailabilityState;
})
.finally(() => {
actionProvidersAvailabilityPromise = null;
});
return actionProvidersAvailabilityPromise;
}
function isZapretInstalledForUi() {
return actionProvidersAvailabilityState.zapretInstalled;
}
function isZapret2InstalledForUi() {
return actionProvidersAvailabilityState.zapret2Installed;
}
function isByedpiInstalledForUi() {
return actionProvidersAvailabilityState.byedpiInstalled;
}
function isWdttInstalledForUi() {
return actionProvidersAvailabilityState.wdttInstalled;
}
function isOlcrtcInstalledForUi() {
return actionProvidersAvailabilityState.olcrtcInstalled;
}
function isSingBoxExtendedForUi() {
return actionProvidersAvailabilityState.singBoxExtended;
}
function getRuleConfiguredAction(section_id) {
const action = uci.get(UCI_PACKAGE, section_id, "action");
return action ? `${action}` : null;
}
function getRuleResolvedAction(section_id) {
return getRuleConfiguredAction(section_id) || "connection";
}
function getActionOptionLabel(action) {
switch (`${action}`) {
case "block":
return "Block";
case "bypass":
return "Bypass";
case "connection":
return "Connection";
case "dns":
return "DNS";
case "vpn":
return "VPN";
case "zapret":
return "Zapret";
case "zapret2":
return "Zapret2";
case "byedpi":
return "ByeDPI";
case "wdtt":
return "WDTT";
case "olcrtc":
return "OlcRTC";
case "awg":
return "AmneziaWG";
case "warp":
return "WARP";
case "anytls":
return "AnyTLS";
case "snell":
return "Snell";
case "mieru":
return "Mieru";
case "sudoku":
return "Sudoku";
case "masque":
return "MASQUE";
case "openvpn":
return "OpenVPN";
case "hosts":
return _("Hosts");
case "outbound":
return _("JSON outbound");
case "proxy":
default:
return "Proxy";
}
}
function getRuleActionDisplayValue(section_id) {
const action = getRuleResolvedAction(section_id);
if (action === "zapret") {
return "Zapret";
}
if (action === "zapret2") {
return "Zapret2";
}
if (action === "byedpi") {
return "ByeDPI";
}
if (action === "wdtt") {
return "WDTT";
}
if (action === "olcrtc") {
return "OlcRTC";
}
if (action === "awg") {
return "AmneziaWG";
}
if (action === "warp") {
return "WARP";
}
if (action === "anytls") {
return "AnyTLS";
}
if (action === "snell") {
return "Snell";
}
if (action === "mieru") {
return "Mieru";
}
if (action === "sudoku") {
return "Sudoku";
}
if (action === "masque") {
return "MASQUE";
}
if (action === "openvpn") {
return "OpenVPN";
}
return getActionOptionLabel(action);
}
function getRuleActionDisplayMarkup(section_id) {
return getRuleActionDisplayValue(section_id);
}
function populateActionOptionValues(option) {
delete option.keylist;
delete option.vallist;
option.value("connection", getActionOptionLabel("connection"));
if (isSingBoxExtendedForUi()) {
option.value("awg", getActionOptionLabel("awg"));
option.value("warp", getActionOptionLabel("warp"));
option.value("masque", getActionOptionLabel("masque"));
option.value("openvpn", getActionOptionLabel("openvpn"));
option.value("anytls", getActionOptionLabel("anytls"));
option.value("snell", getActionOptionLabel("snell"));
option.value("mieru", getActionOptionLabel("mieru"));
option.value("sudoku", getActionOptionLabel("sudoku"));
}
option.value("bypass", "Bypass");
option.value("block", "Block");
option.value("dns", "DNS");
if (isZapretInstalledForUi()) {
option.value("zapret", getActionOptionLabel("zapret"));
}
if (isZapret2InstalledForUi()) {
option.value("zapret2", getActionOptionLabel("zapret2"));
}
if (isByedpiInstalledForUi()) {
option.value("byedpi", getActionOptionLabel("byedpi"));
}
if (isWdttInstalledForUi()) {
option.value("wdtt", getActionOptionLabel("wdtt"));
}
if (isOlcrtcInstalledForUi()) {
option.value("olcrtc", getActionOptionLabel("olcrtc"));
}
option.value("hosts", getActionOptionLabel("hosts"));
}
function getConfigListValues(section_id, key) {
return normalizeOptionValues(uci.get(UCI_PACKAGE, section_id, key));
}
function stringArraysEqual(left, right) {
left = normalizeDynamicListItems(left);
right = normalizeDynamicListItems(right);
return (
left.length === right.length &&
left.every((value, index) => value === right[index])
);
}
function writeListOption(section_id, key, values) {
const normalized = normalizeOptionValues(values);
if (stringArraysEqual(getConfigListValues(section_id, key), normalized)) {
return;
}
if (normalized.length) {
uci.set(UCI_PACKAGE, section_id, key, normalized);
} else {
uci.unset(UCI_PACKAGE, section_id, key);
}
}
function makeDeviceOptionsExclusive(firstOption, secondOption) {
let changing = false;
const firstWidgets = {};
const secondWidgets = {};
function removeMatches(section_id, value, otherWidgets) {
if (changing) {
return;
}
const selected = new Set(normalizeOptionValues(value));
if (!selected.size) {
return;
}
const widget = otherWidgets[section_id];
if (!widget) {
return;
}
const current = normalizeOptionValues(widget.getValue());
const filtered = current.filter((item) => !selected.has(item));
if (stringArraysEqual(current, filtered)) {
return;
}
changing = true;
try {
widget.setValue(filtered);
} finally {
changing = false;
}
}
firstOption.onDeviceWidgetReady = function (section_id, widget) {
firstWidgets[section_id] = widget;
};
secondOption.onDeviceWidgetReady = function (section_id, widget) {
secondWidgets[section_id] = widget;
};
firstOption.onDeviceListChange = function (section_id, value) {
removeMatches(section_id, value, secondWidgets);
};
secondOption.onDeviceListChange = function (section_id, value) {
removeMatches(section_id, value, firstWidgets);
};
}
function childOwnerOption(ownerOption) {
return ownerOption || "section";
}
function childItemOrder(item) {
const value = item ? item.order : null;
const parsed = Number.parseInt(value == null ? "0" : `${value}`, 10);
return Number.isFinite(parsed) ? parsed : 0;
}
function getChildItemIds(section_id, typeName, ownerOption) {
const ownerKey = childOwnerOption(ownerOption);
const items = uci
.sections(UCI_PACKAGE, typeName)
.filter((item) => item[ownerKey] === section_id);
items.sort((a, b) => {
const hasOrderA = a && a.order != null && a.order !== "";
const hasOrderB = b && b.order != null && b.order !== "";
if (hasOrderA && hasOrderB) {
const orderDiff = childItemOrder(a) - childItemOrder(b);
if (orderDiff !== 0) {
return orderDiff;
}
} else if (hasOrderA) {
return -1;
} else if (hasOrderB) {
return 1;
}
if (typeName === "priority_level") {
return `${a[".name"] || ""}`.localeCompare(`${b[".name"] || ""}`);
}
const indexA = a && a[".index"] != null ? a[".index"] : 0;
const indexB = b && b[".index"] != null ? b[".index"] : 0;
return indexA - indexB;
});
return items.map((item) => item[".name"]).filter(Boolean);
}
function childItemValue(itemId, valueOption, fallback) {
if (!valueOption) {
return itemId;
}
const value = uci.get(UCI_PACKAGE, itemId, valueOption);
return value == null || value === "" ? fallback || itemId : `${value}`;
}
function childItemInputValue(
section_id,
value,
typeName,
valueOption,
ownerOption,
) {
const itemId = `${value || ""}`;
if (isExistingChildItem(section_id, itemId, typeName, ownerOption)) {
return childItemValue(itemId, valueOption, itemId);
}
return itemId;
}
function isExistingChildItem(section_id, itemId, typeName, ownerOption) {
const ownerKey = childOwnerOption(ownerOption);
return Boolean(
itemId &&
uci.get(UCI_PACKAGE, itemId, ".type") === typeName &&
uci.get(UCI_PACKAGE, itemId, ownerKey) === section_id,
);
}
function findChildItemForInput(section_id, options, inputValue) {
const rawValue = `${inputValue || ""}`.trim();
if (
isExistingChildItem(
section_id,
rawValue,
options.typeName,
options.ownerOption,
)
) {
return rawValue;
}
if (options.valueOption) {
return getChildItemIds(
section_id,
options.typeName,
options.ownerOption,
).find(
(itemId) =>
childItemValue(itemId, options.valueOption, itemId) === rawValue,
);
}
return null;
}
function createChildItem(section_id, options, inputValue) {
const rawValue = `${inputValue || ""}`.trim();
const existing = findChildItemForInput(section_id, options, rawValue);
if (existing) {
return {
value: existing,
text: options.valueOption
? childItemValue(existing, options.valueOption, existing)
: existing,
created: false,
};
}
const requestedId =
typeof options.createId === "function"
? `${options.createId(rawValue, section_id) || ""}`.trim()
: "";
const itemId =
(requestedId && uci.add(UCI_PACKAGE, options.typeName, requestedId)) ||
requestedId ||
uci.add(UCI_PACKAGE, options.typeName);
uci.set(
UCI_PACKAGE,
itemId,
childOwnerOption(options.ownerOption),
section_id,
);
if (options.valueOption) {
uci.set(UCI_PACKAGE, itemId, options.valueOption, rawValue);
}
if (options.defaults && typeof options.defaults === "object") {
Object.entries(options.defaults).forEach(([key, value]) => {
value =
typeof value === "function"
? value(rawValue, section_id, itemId)
: value;
if (value !== undefined && value !== null && value !== "") {
uci.set(UCI_PACKAGE, itemId, key, `${value}`);
}
});
}
return {
value: itemId,
text: options.valueOption ? rawValue : itemId,
created: true,
};
}
function applyChildItemSettings(itemId, settings) {
Object.entries(settings || {}).forEach(([key, value]) => {
if (!key || key.charAt(0) === ".") {
return;
}
if (value === undefined || value === null || value === "") {
uci.unset(UCI_PACKAGE, itemId, key);
} else if (Array.isArray(value)) {
uci.set(
UCI_PACKAGE,
itemId,
key,
value
.map((item) => `${item || ""}`.trim())
.filter((item) => item.length),
);
} else {
uci.set(UCI_PACKAGE, itemId, key, `${value}`);
}
});
}
function materializeChildItems(section_id, options, inputValue) {
const result = [];
const seen = new Set();
normalizeDynamicListItems(inputValue).forEach((value) => {
const createdItem = createChildItem(section_id, options, value);
const itemId = createdItem.value;
const stagedSettings =
createdItem.created && typeof options.stagedSettings === "function"
? options.stagedSettings(value, itemId, createdItem.created)
: null;
if (stagedSettings) {
applyChildItemSettings(itemId, stagedSettings);
}
if (itemId && !seen.has(itemId)) {
seen.add(itemId);
result.push(itemId);
}
});
return result;
}
function cleanupPriorityLevelsForGroup(groupId) {
getChildItemIds(groupId, "priority_level", "group").forEach((levelId) => {
uci.remove(UCI_PACKAGE, levelId);
});
}
function cleanupRemovedChildItems(
section_id,
typeName,
keepValues,
ownerOption,
) {
const keep = new Set(normalizeDynamicListItems(keepValues));
getChildItemIds(section_id, typeName, ownerOption).forEach((itemId) => {
if (!keep.has(itemId)) {
if (typeName === "priority_group") {
cleanupPriorityLevelsForGroup(itemId);
}
uci.remove(UCI_PACKAGE, itemId);
}
});
}
function parentSectionIdForItem(itemId) {
return uci.get(UCI_PACKAGE, itemId, "section") || "";
}
function refreshOptionChoices(option, choices) {
delete option.keylist;
delete option.vallist;
(choices || []).forEach((choice) => {
if (typeof choice === "object") {
option.value(choice.value, choice.label);
} else {
option.value(choice);
}
});
}
function validateRegex(_section_id, value) {
if (!value || !value.length) {
return true;
}
try {
new RegExp(value);
return true;
} catch (_error) {
return _("Invalid regular expression");
}
}
function validateKeyword(_section_id, value) {
if (!value || !value.length) {
return true;
}
if (/[,\s]/.test(value)) {
return _("Keyword must not contain spaces or commas");
}
return true;
}
function isSingBoxDuration(value) {
return /^(?=.*[1-9])([0-9]+(?:\.[0-9]+)?(?:ns|us|ms|s|m|h|d))+$/.test(value);
}
function writeOptionalDurationOption(section_id, key, value) {
const normalized = value ? `${value}`.trim() : "";
const disabledKey = `${key}_disabled`;
if (normalized.length) {
uci.set(UCI_PACKAGE, section_id, key, normalized);
uci.unset(UCI_PACKAGE, section_id, disabledKey);
} else {
uci.unset(UCI_PACKAGE, section_id, key);
uci.set(UCI_PACKAGE, section_id, disabledKey, "1");
}
}
function validateOptionalSingBoxDuration(value) {
const normalized = value ? `${value}`.trim() : "";
if (!normalized.length) {
return true;
}
if (isSingBoxDuration(normalized)) {
return true;
}
return _("Use sing-box duration format like 1d, 12h or 30m");
}
function validateRequiredSingBoxDuration(value) {
const normalized = value ? `${value}`.trim() : "";
if (!normalized.length) {
return _("Use sing-box duration format like 1d, 12h or 30m");
}
if (isSingBoxDuration(normalized)) {
return true;
}
return _("Use sing-box duration format like 1d, 12h or 30m");
}
function parseSubscriptionUrlEntry(value) {
const normalized = value ? `${value}`.trim() : "";
if (!normalized.length) {
return { valid: true, url: "" };
}
if (normalized.includes("|")) {
return {
valid: false,
message: _("Configure User-Agent in the item settings"),
};
}
return { valid: true, url: normalized };
}
function validateSubscriptionUrlEntry(_section_id, value) {
if (!value || value.length === 0) {
return true;
}
const parsed = parseSubscriptionUrlEntry(value);
if (!parsed.valid) {
return parsed.message;
}
const validation = main.validateUrl(parsed.url);
return validation.valid ? true : validation.message;
}
function getDuplicateTextListErrors(values, normalizeValue, duplicateMessage) {
const seen = new Set();
const duplicates = [];
values.forEach((item) => {
const normalized = normalizeValue ? normalizeValue(item) : item;
if (seen.has(normalized)) {
if (!duplicates.includes(item)) {
duplicates.push(item);
}
return;
}
seen.add(normalized);
});
return duplicates.map((item) => `${item}: ${duplicateMessage}`);
}
function getValidationHeaderText() {
return _("Validation errors:");
}
function getDuplicateValueText() {
return _("Duplicate value");
}
function escapeHtml(value) {
return `${value}`
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function ensureAnnotatedTextareaStyles() {
if (
typeof document === "undefined" ||
!document.head ||
document.getElementById(ANNOTATED_TEXTAREA_STYLE_ID)
) {
return;
}
document.head.insertAdjacentHTML(
"beforeend",
`<style id="${ANNOTATED_TEXTAREA_STYLE_ID}">
.fkp-annotated-textarea {
position: relative;
}
.fkp-annotated-textarea > textarea {
position: relative;
z-index: 1;
background: transparent !important;
}
.fkp-annotated-textarea__overlay {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
overflow: hidden;
box-sizing: border-box;
color: transparent;
white-space: pre-wrap;
word-break: break-word;
overflow-wrap: break-word;
}
.fkp-annotated-textarea__invalid {
color: transparent;
text-decoration-line: underline;
text-decoration-style: wavy;
text-decoration-color: var(--error-color-medium, #d44);
text-decoration-thickness: 1.5px;
text-underline-offset: 2px;
text-decoration-skip-ink: none;
}
</style>`,
);
}
function applyTextareaInputAttributes(textarea) {
textarea.setAttribute("spellcheck", "false");
textarea.setAttribute("autocomplete", "off");
textarea.setAttribute("autocorrect", "off");
textarea.setAttribute("autocapitalize", "off");
textarea.setAttribute("data-gramm", "false");
textarea.setAttribute("data-gramm_editor", "false");
textarea.setAttribute("data-enable-grammarly", "false");
textarea.style.resize = "vertical";
textarea.style.maxWidth = "100%";
const getRowsMinHeight = () => {
const rows = Number.parseInt(textarea.getAttribute("rows") || "0", 10);
if (!rows || typeof window === "undefined") {
return 0;
}
const style = window.getComputedStyle(textarea);
const fontSize = Number.parseFloat(style.fontSize) || 16;
const lineHeight =
Number.parseFloat(style.lineHeight) || Math.ceil(fontSize * 1.2);
const verticalPadding =
(Number.parseFloat(style.paddingTop) || 0) +
(Number.parseFloat(style.paddingBottom) || 0);
const verticalBorder =
(Number.parseFloat(style.borderTopWidth) || 0) +
(Number.parseFloat(style.borderBottomWidth) || 0);
return Math.ceil(rows * lineHeight + verticalPadding + verticalBorder);
};
const applyMinHeight = () => {
const storedMinHeight = Number.parseFloat(
textarea.getAttribute("data-fkp-default-min-height") || "0",
);
const nextMinHeight = Math.max(
storedMinHeight,
textarea.offsetHeight,
getRowsMinHeight(),
);
if (nextMinHeight > 0) {
if (storedMinHeight <= 0) {
textarea.setAttribute(
"data-fkp-default-min-height",
`${nextMinHeight}`,
);
}
textarea.style.minHeight = `${nextMinHeight}px`;
return true;
}
return false;
};
if (
!applyMinHeight() &&
typeof window !== "undefined" &&
typeof window.requestAnimationFrame === "function"
) {
window.requestAnimationFrame(() => {
if (!applyMinHeight()) {
window.setTimeout(applyMinHeight, 0);
}
});
}
textarea.addEventListener("focus", applyMinHeight);
textarea.addEventListener("pointerdown", applyMinHeight);
}
function syncAnnotatedTextareaOverlay(textarea, wrapper, overlay) {
if (
typeof window === "undefined" ||
!textarea ||
!wrapper ||
!overlay ||
typeof window.getComputedStyle !== "function"
) {
return;
}
const style = window.getComputedStyle(textarea);
wrapper.style.backgroundColor = style.backgroundColor;
wrapper.style.borderRadius = style.borderRadius;
overlay.style.font = style.font;
overlay.style.lineHeight = style.lineHeight;
overlay.style.letterSpacing = style.letterSpacing;
overlay.style.paddingTop = style.paddingTop;
overlay.style.paddingRight = style.paddingRight;
overlay.style.paddingBottom = style.paddingBottom;
overlay.style.paddingLeft = style.paddingLeft;
overlay.style.borderTopWidth = style.borderTopWidth;
overlay.style.borderRightWidth = style.borderRightWidth;
overlay.style.borderBottomWidth = style.borderBottomWidth;
overlay.style.borderLeftWidth = style.borderLeftWidth;
overlay.style.borderStyle = "solid";
overlay.style.borderColor = "transparent";
overlay.style.textAlign = style.textAlign;
overlay.style.direction = style.direction;
overlay.style.tabSize = style.tabSize;
overlay.style.textIndent = style.textIndent;
overlay.style.textTransform = style.textTransform;
overlay.style.boxSizing = style.boxSizing;
overlay.style.scrollPaddingTop = style.scrollPaddingTop;
overlay.scrollTop = textarea.scrollTop;
overlay.scrollLeft = textarea.scrollLeft;
}
function createAnnotationKey(annotation) {
return `${annotation.start}:${annotation.end}`;
}
function addAnnotationIssue(annotationMap, annotation, message) {
const key = createAnnotationKey(annotation);
const existing = annotationMap.get(key);
if (existing) {
if (!existing.messages.includes(message)) {
existing.messages.push(message);
}
return;
}
annotationMap.set(key, {
start: annotation.start,
end: annotation.end,
messages: [message],
});
}
function finalizeAnnotations(annotationMap) {
return Array.from(annotationMap.values())
.map((annotation) => ({
start: annotation.start,
end: annotation.end,
message: annotation.messages.join("; "),
}))
.sort((left, right) => left.start - right.start || left.end - right.end);
}
function renderAnnotatedTextareaOverlay(value, annotations) {
const text = value ? `${value}` : "";
const normalizedAnnotations = Array.isArray(annotations) ? annotations : [];
if (!text.length) {
return "&#8203;";
}
if (!normalizedAnnotations.length) {
return `${escapeHtml(text)}${text.endsWith("\n") ? "\n " : ""}`;
}
let cursor = 0;
let html = "";
normalizedAnnotations.forEach((annotation) => {
if (
annotation.start < cursor ||
annotation.start >= annotation.end ||
annotation.start < 0
) {
return;
}
html += escapeHtml(text.slice(cursor, annotation.start));
html += `<span class="fkp-annotated-textarea__invalid">${escapeHtml(
text.slice(annotation.start, annotation.end),
)}</span>`;
cursor = annotation.end;
});
html += escapeHtml(text.slice(cursor));
if (text.endsWith("\n")) {
html += "\n ";
}
return html;
}
function attachAnnotatedTextarea(textarea, analyzer) {
if (!textarea || typeof analyzer !== "function") {
return;
}
ensureAnnotatedTextareaStyles();
if (textarea.__tachyonAnnotatedTextareaController) {
textarea.__tachyonAnnotatedTextareaController.analyzer = analyzer;
textarea.__tachyonAnnotatedTextareaController.update();
return;
}
const wrapper = textarea.parentNode;
if (!wrapper) {
return;
}
wrapper.classList.add("fkp-annotated-textarea");
const overlay = document.createElement("div");
overlay.className = "fkp-annotated-textarea__overlay";
overlay.setAttribute("aria-hidden", "true");
wrapper.insertBefore(overlay, textarea.nextSibling);
const controller = {
analyzer,
textarea,
wrapper,
overlay,
update() {
const analysis = this.analyzer(this.textarea.value);
this.overlay.innerHTML = renderAnnotatedTextareaOverlay(
this.textarea.value,
analysis.annotations,
);
syncAnnotatedTextareaOverlay(this.textarea, this.wrapper, this.overlay);
},
};
textarea.__tachyonAnnotatedTextareaController = controller;
const updateAnnotatedTextarea = () => controller.update();
textarea.addEventListener("input", updateAnnotatedTextarea);
textarea.addEventListener("change", updateAnnotatedTextarea);
textarea.addEventListener("scroll", updateAnnotatedTextarea, {
passive: true,
});
textarea.addEventListener("keyup", updateAnnotatedTextarea);
if (typeof ResizeObserver === "function") {
const resizeObserver = new ResizeObserver(() => controller.update());
resizeObserver.observe(textarea);
controller.resizeObserver = resizeObserver;
}
controller.update();
}
function refreshAnnotatedTextareaValidation(option, section_id, textarea) {
if (option && typeof option.triggerValidation === "function") {
option.triggerValidation(section_id);
}
if (
textarea &&
textarea.__tachyonAnnotatedTextareaController &&
typeof textarea.__tachyonAnnotatedTextareaController.update === "function"
) {
textarea.__tachyonAnnotatedTextareaController.update();
}
}
function attachNfqwsRemoteValidation(option, section_id, textarea) {
if (!textarea || textarea.__tachyonNfqwsRemoteValidationAttached) {
return;
}
textarea.__tachyonNfqwsRemoteValidationAttached = true;
textarea.__tachyonNfqwsRemoteValidationRequestId = 0;
textarea.__tachyonNfqwsRemoteValidationTimer = null;
const runValidation = () => {
const value = textarea.value;
const localAnalysis = buildNfqwsLocalAnalysis(value);
if (!localAnalysis.valid) {
refreshAnnotatedTextareaValidation(option, section_id, textarea);
return;
}
const requestId =
(textarea.__tachyonNfqwsRemoteValidationRequestId || 0) + 1;
textarea.__tachyonNfqwsRemoteValidationRequestId = requestId;
validateNfqwsStrategyRemotely(value).then(() => {
if (textarea.__tachyonNfqwsRemoteValidationRequestId !== requestId) {
return;
}
refreshAnnotatedTextareaValidation(option, section_id, textarea);
});
};
const scheduleValidation = (delay = NFQWS_REMOTE_VALIDATION_DEBOUNCE_MS) => {
if (textarea.__tachyonNfqwsRemoteValidationTimer) {
window.clearTimeout(textarea.__tachyonNfqwsRemoteValidationTimer);
}
textarea.__tachyonNfqwsRemoteValidationTimer = window.setTimeout(() => {
textarea.__tachyonNfqwsRemoteValidationTimer = null;
runValidation();
}, delay);
};
textarea.addEventListener("input", () => scheduleValidation());
textarea.addEventListener("change", () => scheduleValidation(0));
textarea.addEventListener("blur", () => scheduleValidation(0));
scheduleValidation(0);
}
function attachNfqws2RemoteValidation(option, section_id, textarea) {
if (!textarea || textarea.__tachyonNfqws2RemoteValidationAttached) {
return;
}
textarea.__tachyonNfqws2RemoteValidationAttached = true;
textarea.__tachyonNfqws2RemoteValidationRequestId = 0;
textarea.__tachyonNfqws2RemoteValidationTimer = null;
const runValidation = () => {
const value = textarea.value;
const localAnalysis = buildNfqws2LocalAnalysis(value);
if (!localAnalysis.valid) {
refreshAnnotatedTextareaValidation(option, section_id, textarea);
return;
}
const requestId =
(textarea.__tachyonNfqws2RemoteValidationRequestId || 0) + 1;
textarea.__tachyonNfqws2RemoteValidationRequestId = requestId;
validateNfqws2StrategyRemotely(value).then(() => {
if (textarea.__tachyonNfqws2RemoteValidationRequestId !== requestId) {
return;
}
refreshAnnotatedTextareaValidation(option, section_id, textarea);
});
};
const scheduleValidation = (delay = NFQWS_REMOTE_VALIDATION_DEBOUNCE_MS) => {
if (textarea.__tachyonNfqws2RemoteValidationTimer) {
window.clearTimeout(textarea.__tachyonNfqws2RemoteValidationTimer);
}
textarea.__tachyonNfqws2RemoteValidationTimer = window.setTimeout(() => {
textarea.__tachyonNfqws2RemoteValidationTimer = null;
runValidation();
}, delay);
};
textarea.addEventListener("input", () => scheduleValidation());
textarea.addEventListener("change", () => scheduleValidation(0));
textarea.addEventListener("blur", () => scheduleValidation(0));
scheduleValidation(0);
}
function parseCommentAwareListTokens(value) {
const text = value ? `${value}` : "";
const tokens = [];
const lines = text.split(/\r\n|\r|\n/);
const newlines = text.match(/\r\n|\r|\n/g) || [];
let offset = 0;
lines.forEach((line, index) => {
const cleanLine = line.replace(
/^(full|keyword|regex):[ \t]*(\/\/|#).*$/,
"",
);
const hashIndex = cleanLine.indexOf("#");
const slashIndex = cleanLine.indexOf("//");
let commentIndex = -1;
if (hashIndex >= 0 && slashIndex >= 0) {
commentIndex = Math.min(hashIndex, slashIndex);
} else if (hashIndex >= 0) {
commentIndex = hashIndex;
} else if (slashIndex >= 0) {
commentIndex = slashIndex;
}
const source =
commentIndex >= 0 ? cleanLine.slice(0, commentIndex) : cleanLine;
const matcher = /[^,\s]+/g;
let match;
while ((match = matcher.exec(source)) !== null) {
tokens.push({
value: match[0],
start: offset + match.index,
end: offset + match.index + match[0].length,
});
}
offset += line.length + (newlines[index] ? newlines[index].length : 0);
});
return tokens;
}
function analyzeTextListValue(value, validateItem, emptyMessage, options = {}) {
const text = value ? `${value}` : "";
if (!text.length) {
return { valid: true, message: "", annotations: [] };
}
const tokens = parseCommentAwareListTokens(text);
if (!tokens.length) {
return { valid: false, message: emptyMessage, annotations: [] };
}
const duplicateMessage = options.duplicateMessage || getDuplicateValueText();
const annotationMap = new Map();
const errors = [];
const seen = new Set();
tokens.forEach((token) => {
if (typeof validateItem === "function") {
const validation = validateItem(token.value);
if (!validation.valid) {
errors.push(`${token.value}: ${validation.message}`);
addAnnotationIssue(annotationMap, token, validation.message);
}
}
const normalized = options.normalizeDuplicateValue
? options.normalizeDuplicateValue(token.value)
: token.value;
if (!normalized) {
return;
}
if (seen.has(normalized)) {
errors.push(`${token.value}: ${duplicateMessage}`);
addAnnotationIssue(annotationMap, token, duplicateMessage);
return;
}
seen.add(normalized);
});
if (!errors.length) {
return { valid: true, message: "", annotations: [] };
}
return {
valid: false,
message: [getValidationHeaderText(), ...errors].join("\n"),
annotations: finalizeAnnotations(annotationMap),
};
}
function analyzeDomainSuffixText(value) {
const validateDomainCondition = (domain) => {
const normalized = `${domain || ""}`.trim();
if (normalized.includes("/")) {
return { valid: false, message: _("Invalid domain address") };
}
return main.validateDomain(normalized, true);
};
return analyzeTextListValue(
value,
(item) => {
const colonIndex = item.indexOf(":");
const prefix = colonIndex > 0 ? item.slice(0, colonIndex) : "";
const body = colonIndex > 0 ? item.slice(colonIndex + 1) : item;
if (!prefix) {
return validateDomainCondition(body);
}
if (!["full", "keyword", "regex"].includes(prefix)) {
return {
valid: false,
message: _("Allowed domain prefixes are full:, keyword:, and regex:"),
};
}
if (!body.length) {
return { valid: false, message: _("Value cannot be empty") };
}
if (prefix === "full") {
return validateDomainCondition(body);
}
if (prefix === "keyword") {
const validation = validateKeyword(null, body);
return validation === true
? { valid: true, message: _("Valid") }
: { valid: false, message: validation };
}
if (/[,\s]/.test(body)) {
return {
valid: false,
message: _("Regular expression must not contain spaces or commas"),
};
}
const validation = validateRegex(null, body);
return validation === true
? { valid: true, message: _("Valid") }
: { valid: false, message: validation };
},
_("At least one valid domain must be specified."),
{
normalizeDuplicateValue: (item) => `${item}`.toLowerCase(),
},
);
}
function domainValuesWithPrefix(section_id, key, prefix) {
return getConfigListValues(section_id, key).map((value) =>
prefix ? `${prefix}:${value}` : value,
);
}
function domainTextValuesWithPrefix(section_id, key, prefix) {
const legacyText = uci.get(UCI_PACKAGE, section_id, `${key}_text`);
if (!legacyText) {
return [];
}
return main
.parseValueList(legacyText)
.map((value) => (prefix ? `${prefix}:${value}` : value));
}
function uniqueDomainTextValues(values) {
const seen = new Set();
return values.filter((value) => {
const key = `${value}`.toLowerCase();
if (!key || seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
function appendUniqueDomainTextValues(textValue, values) {
const originalText = typeof textValue === "string" ? textValue : "";
const seen = new Set(
main.parseValueList(originalText).map((value) => `${value}`.toLowerCase()),
);
const additions = uniqueDomainTextValues(values).filter((value) => {
const key = `${value}`.toLowerCase();
if (!key || seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
if (!additions.length) {
return originalText;
}
const base = originalText.replace(/\s+$/, "");
return [base, ...additions].filter(Boolean).join("\n");
}
function stripDomainFullPrefix(val) {
if (!val) return "";
const lines = typeof val === "string" ? val.split("\n") : Array.isArray(val) ? val : [String(val)];
return lines
.map((line) => {
const trimmed = String(line || "").trim();
if (trimmed.startsWith("full:")) {
const body = trimmed.substring(5).trim();
if (body && !body.includes("/") && !body.includes(":") && !body.includes(" ") && !body.includes(",")) {
return body;
}
}
return trimmed;
})
.filter(Boolean)
.join("\n");
}
function loadCombinedDomainText(section_id) {
const rawTextValue =
uci.get(UCI_PACKAGE, section_id, "domain") ||
uci.get(UCI_PACKAGE, section_id, "domain_suffix_text");
const textValue = stripDomainFullPrefix(rawTextValue);
const values = [
...domainValuesWithPrefix(section_id, "domain_suffix", ""),
...domainValuesWithPrefix(section_id, "domain_keyword", "keyword"),
...domainValuesWithPrefix(section_id, "domain_regex", "regex"),
...domainTextValuesWithPrefix(section_id, "domain_suffix", ""),
...domainTextValuesWithPrefix(section_id, "domain", ""),
...domainTextValuesWithPrefix(section_id, "domain_keyword", "keyword"),
...domainTextValuesWithPrefix(section_id, "domain_regex", "regex"),
];
return appendUniqueDomainTextValues(textValue, values);
}
function analyzeIpCidrText(value) {
return analyzeTextListValue(
value,
(item) => main.validateSubnet(item),
_("At least one valid IP or subnet must be specified."),
{
normalizeDuplicateValue: (item) => `${item}`.trim(),
},
);
}
function validatePortCondition(_section_id, value) {
const normalized = value ? `${value}`.trim() : "";
if (!normalized.length) {
return true;
}
const match = normalized.match(/^(\d+)(?:-(\d+))?$/);
if (!match) {
return _("Invalid port or range. Use 80 or 1000-2000");
}
const start = Number.parseInt(match[1], 10);
const end = match[2] ? Number.parseInt(match[2], 10) : start;
if (start < 1 || start > 65535 || end < 1 || end > 65535) {
return _("Port must be between 1 and 65535");
}
if (start > end) {
return _("Port range start must be less than or equal to end");
}
return true;
}
function getNfqwsOptionArgumentMode(option) {
if (NFQWS_REQUIRED_ARG_OPTIONS.has(option)) {
return "required";
}
if (NFQWS_OPTIONAL_ARG_OPTIONS.has(option)) {
return "optional";
}
if (NFQWS_NO_ARG_OPTIONS.has(option)) {
return "none";
}
return "unknown";
}
function getNfqws2OptionArgumentMode(option) {
if (NFQWS2_REQUIRED_ARG_OPTIONS.has(option)) {
return "required";
}
if (NFQWS2_OPTIONAL_ARG_OPTIONS.has(option)) {
return "optional";
}
if (NFQWS2_NO_ARG_OPTIONS.has(option)) {
return "none";
}
return "unknown";
}
function normalizeNfqwsStrategyWhitespace(value) {
return value ? `${value}`.replace(/\s+/g, " ").trim() : "";
}
function parseNfqwsRuntimeTokens(value) {
const text = value ? `${value}` : "";
const tokens = [];
const matcher = /\S+/g;
let match;
while ((match = matcher.exec(text)) !== null) {
tokens.push({
value: match[0],
start: match.index,
end: match.index + match[0].length,
});
}
return tokens;
}
function normalizeNfqwsStrategyValue(value) {
const normalized = normalizeNfqwsStrategyWhitespace(value);
if (!normalized.length) {
return "";
}
return normalized === ZAPRET_LEGACY_DEFAULT_NFQWS_OPT
? ZAPRET_DEFAULT_NFQWS_OPT
: normalized;
}
function getCachedNfqwsRemoteValidation(value) {
const normalized = normalizeNfqwsStrategyValue(value);
return normalized.length
? nfqwsRemoteValidationCache.get(normalized) || null
: null;
}
function cacheNfqwsRemoteValidation(value, result) {
const normalized = normalizeNfqwsStrategyValue(value);
if (!normalized.length) {
return result;
}
const cached = {
valid: result && result.valid === true,
message: result && result.message ? `${result.message}` : "",
needle: result && result.needle ? `${result.needle}` : "",
needles:
result && Array.isArray(result.needles)
? result.needles.filter(Boolean).map((item) => `${item}`)
: result && result.needle
? [`${result.needle}`]
: [],
};
nfqwsRemoteValidationCache.set(normalized, cached);
return cached;
}
function buildNfqwsRemoteValidationFallback(error) {
const message =
error && error.message
? `${error.message}`
: _("Unable to validate the NFQWS strategy through the backend parser.");
return {
valid: false,
message: _("Backend validation failed: %s").format(message),
needle: "",
needles: [],
};
}
function validateNfqwsStrategyRemotely(value) {
const normalized = normalizeNfqwsStrategyValue(value);
if (!normalized.length) {
return Promise.resolve({
valid: true,
message: "",
needle: "",
needles: [],
});
}
if (nfqwsRemoteValidationCache.has(normalized)) {
return Promise.resolve(nfqwsRemoteValidationCache.get(normalized));
}
if (nfqwsRemoteValidationInflight.has(normalized)) {
return nfqwsRemoteValidationInflight.get(normalized);
}
const validationTask = fs
.exec(NFQWS_VALIDATION_COMMAND, [
"validate_nfqws_strategy_json",
normalized,
])
.then((result) => {
const payload = JSON.parse(
(result && result.stdout ? result.stdout : "{}").trim() || "{}",
);
return cacheNfqwsRemoteValidation(normalized, {
valid: payload.valid === true,
message: payload.message || "",
needle: payload.needle || "",
needles: Array.isArray(payload.needles)
? payload.needles.filter(Boolean)
: payload.needle
? [payload.needle]
: [],
});
})
.catch((error) =>
cacheNfqwsRemoteValidation(
normalized,
buildNfqwsRemoteValidationFallback(error),
),
)
.finally(() => {
nfqwsRemoteValidationInflight.delete(normalized);
});
nfqwsRemoteValidationInflight.set(normalized, validationTask);
return validationTask;
}
function getNfqwsForbiddenTokenInfo(token, index) {
const configFileMessage = _(
"External nfqws config files bypass Tachyon queue management and explicit validation.",
);
const hostSelectionMessage = _(
"Resource selection by hostname inside nfqws is not supported here; sing-box selects resources before NFQUEUE.",
);
const ipSelectionMessage = _(
"Resource selection by IP or CIDR inside nfqws is not supported here; sing-box selects resources before NFQUEUE.",
);
const placeholderMessage = _(
"Zapret hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS strategies.",
);
const queueMessage = _(
"The NFQUEUE number is assigned by Tachyon for each rule and must not be overridden here.",
);
const fwmarkMessage = _(
"The desync fwmark is managed by Tachyon for loop prevention and must not be overridden here.",
);
const daemonMessage = _(
"Tachyon manages the nfqws process lifecycle itself, so daemon mode is not allowed here.",
);
const dryRunMessage = _(
"This field must start a working nfqws strategy; --dry-run exits immediately and is not allowed here.",
);
const versionMessage = _(
"This field must start a working nfqws strategy; --version exits immediately and is not allowed here.",
);
if (index === 0 && (token.startsWith("@") || token.startsWith("$"))) {
return {
reason: configFileMessage,
captureNextValue: false,
};
}
if (token === "<HOSTLIST>" || token === "<HOSTLIST_NOAUTO>") {
return {
reason: placeholderMessage,
captureNextValue: false,
};
}
if (
token === "--hostlist" ||
token.startsWith("--hostlist=") ||
token === "--hostlist-domains" ||
token.startsWith("--hostlist-domains=") ||
token === "--hostlist-exclude" ||
token.startsWith("--hostlist-exclude=") ||
token === "--hostlist-exclude-domains" ||
token.startsWith("--hostlist-exclude-domains=") ||
token === "--hostlist-auto" ||
token.startsWith("--hostlist-auto=") ||
token === "--hostlist-auto-fail-threshold" ||
token.startsWith("--hostlist-auto-fail-threshold=") ||
token === "--hostlist-auto-fail-time" ||
token.startsWith("--hostlist-auto-fail-time=") ||
token === "--hostlist-auto-retrans-threshold" ||
token.startsWith("--hostlist-auto-retrans-threshold=") ||
token === "--hostlist-auto-debug" ||
token.startsWith("--hostlist-auto-debug=")
) {
return {
reason: hostSelectionMessage,
captureNextValue: !token.includes("="),
};
}
if (
token === "--ipset" ||
token.startsWith("--ipset=") ||
token === "--ipset-ip" ||
token.startsWith("--ipset-ip=") ||
token === "--ipset-exclude" ||
token.startsWith("--ipset-exclude=") ||
token === "--ipset-exclude-ip" ||
token.startsWith("--ipset-exclude-ip=")
) {
return {
reason: ipSelectionMessage,
captureNextValue: !token.includes("="),
};
}
if (token === "--qnum" || token.startsWith("--qnum=")) {
return {
reason: queueMessage,
captureNextValue: !token.includes("="),
};
}
if (
token === "--dpi-desync-fwmark" ||
token.startsWith("--dpi-desync-fwmark=")
) {
return {
reason: fwmarkMessage,
captureNextValue: !token.includes("="),
};
}
if (token === "--daemon") {
return {
reason: daemonMessage,
captureNextValue: false,
};
}
if (token === "--dry-run") {
return {
reason: dryRunMessage,
captureNextValue: false,
};
}
if (token === "--version") {
return {
reason: versionMessage,
captureNextValue: false,
};
}
return null;
}
function buildNfqwsLocalAnalysis(value) {
const text = value ? `${value}` : "";
if (!text.trim().length) {
return {
valid: false,
message: _("NFQWS strategy cannot be empty"),
annotations: [],
};
}
if (text.trim() === ZAPRET_LEGACY_DEFAULT_NFQWS_OPT) {
return { valid: true, message: "", annotations: [] };
}
const tokens = parseNfqwsRuntimeTokens(text);
const annotationMap = new Map();
const errors = [];
for (let index = 0; index < tokens.length; ) {
const token = tokens[index];
const bareToken = token.value.includes("=")
? token.value.slice(0, token.value.indexOf("="))
: token.value;
const nextToken = tokens[index + 1] || null;
const forbidden = getNfqwsForbiddenTokenInfo(token.value, index);
if (forbidden) {
addAnnotationIssue(annotationMap, token, forbidden.reason);
let displayToken = token.value;
if (
forbidden.captureNextValue &&
nextToken &&
!nextToken.value.startsWith("--")
) {
addAnnotationIssue(annotationMap, nextToken, forbidden.reason);
displayToken = `${displayToken} ${nextToken.value}`;
index += 2;
} else {
index += 1;
}
errors.push(`${displayToken}: ${forbidden.reason}`);
continue;
}
if (!token.value.startsWith("--")) {
const reason = _(
"Unexpected standalone token. Use explicit flags such as --name or --name=value.",
);
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
index += 1;
continue;
}
const mode = getNfqwsOptionArgumentMode(bareToken);
if (mode === "unknown") {
const reason = _("Unknown NFQWS flag.");
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
index += 1;
continue;
}
if (mode === "none") {
if (token.value.includes("=")) {
const reason = _("This flag does not accept a value.");
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
}
index += 1;
continue;
}
if (mode === "optional") {
if (
nextToken &&
!token.value.includes("=") &&
!nextToken.value.startsWith("--")
) {
const reason = _(
"Optional values must be attached with '=' here; a separate token would be ignored by nfqws.",
);
addAnnotationIssue(annotationMap, token, reason);
addAnnotationIssue(annotationMap, nextToken, reason);
errors.push(`${token.value} ${nextToken.value}: ${reason}`);
index += 2;
} else {
index += 1;
}
continue;
}
if (!token.value.includes("=")) {
if (!nextToken || nextToken.value.startsWith("--")) {
const reason = _("This option requires a value.");
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
index += 1;
continue;
}
index += 2;
continue;
}
index += 1;
}
if (!errors.length) {
return { valid: true, message: "", annotations: [] };
}
return {
valid: false,
message: [getValidationHeaderText(), ...errors].join("\n"),
annotations: finalizeAnnotations(annotationMap),
};
}
function addNfqwsRemoteValidationNeedleAnnotations(
annotationMap,
tokens,
remoteValidation,
needle,
) {
if (!needle.length) {
return;
}
let matched = false;
tokens.forEach((token) => {
const tokenValue = token.value || "";
const optionMatch =
needle.startsWith("--") &&
(tokenValue === needle || tokenValue.startsWith(`${needle}=`));
const valueMatch =
tokenValue === needle ||
tokenValue.endsWith(`=${needle}`) ||
(!needle.startsWith("--") && tokenValue.includes(`=${needle},`)) ||
(!needle.startsWith("--") && tokenValue.endsWith(`=${needle}`));
if (optionMatch || valueMatch) {
addAnnotationIssue(annotationMap, token, remoteValidation.message);
matched = true;
}
});
if (matched) {
return;
}
if (needle.startsWith("--")) {
tokens
.filter((token) => token.value && token.value.startsWith(needle))
.forEach((token) =>
addAnnotationIssue(annotationMap, token, remoteValidation.message),
);
}
}
function addNfqwsRemoteValidationAnnotations(
annotationMap,
tokens,
remoteValidation,
) {
const needles =
remoteValidation &&
Array.isArray(remoteValidation.needles) &&
remoteValidation.needles.length
? remoteValidation.needles.map((needle) => `${needle}`)
: remoteValidation && remoteValidation.needle
? [`${remoteValidation.needle}`]
: [];
needles.forEach((needle) =>
addNfqwsRemoteValidationNeedleAnnotations(
annotationMap,
tokens,
remoteValidation,
needle,
),
);
}
function analyzeNfqwsStrategy(value) {
const localAnalysis = buildNfqwsLocalAnalysis(value);
if (!localAnalysis.valid) {
return localAnalysis;
}
const remoteValidation = getCachedNfqwsRemoteValidation(value);
if (!remoteValidation || remoteValidation.valid) {
return localAnalysis;
}
const text = value ? `${value}` : "";
const tokens = parseNfqwsRuntimeTokens(text);
const annotationMap = new Map();
localAnalysis.annotations.forEach((annotation) =>
addAnnotationIssue(annotationMap, annotation, annotation.message),
);
addNfqwsRemoteValidationAnnotations(annotationMap, tokens, remoteValidation);
return {
valid: false,
message: [getValidationHeaderText(), remoteValidation.message].join("\n"),
annotations: finalizeAnnotations(annotationMap),
};
}
function normalizeNfqws2StrategyValue(value) {
const normalized = normalizeNfqwsStrategyWhitespace(value);
return normalized.length ? normalized : ZAPRET2_DEFAULT_NFQWS2_OPT;
}
function getCachedNfqws2RemoteValidation(value) {
const normalized = normalizeNfqws2StrategyValue(value);
return normalized.length
? nfqws2RemoteValidationCache.get(normalized) || null
: null;
}
function cacheNfqws2RemoteValidation(value, result) {
const normalized = normalizeNfqws2StrategyValue(value);
if (!normalized.length) {
return result;
}
const cached = {
valid: result && result.valid === true,
message: result && result.message ? `${result.message}` : "",
needle: result && result.needle ? `${result.needle}` : "",
needles:
result && Array.isArray(result.needles)
? result.needles.filter(Boolean).map((item) => `${item}`)
: result && result.needle
? [`${result.needle}`]
: [],
};
nfqws2RemoteValidationCache.set(normalized, cached);
return cached;
}
function buildNfqws2RemoteValidationFallback(error) {
const message =
error && error.message
? `${error.message}`
: _("Unable to validate the NFQWS2 strategy through the backend parser.");
return {
valid: false,
message: _("Backend validation failed: %s").format(message),
needle: "",
needles: [],
};
}
function validateNfqws2StrategyRemotely(value) {
const normalized = normalizeNfqws2StrategyValue(value);
if (!normalized.length) {
return Promise.resolve({
valid: true,
message: "",
needle: "",
needles: [],
});
}
if (nfqws2RemoteValidationCache.has(normalized)) {
return Promise.resolve(nfqws2RemoteValidationCache.get(normalized));
}
if (nfqws2RemoteValidationInflight.has(normalized)) {
return nfqws2RemoteValidationInflight.get(normalized);
}
const validationTask = fs
.exec(NFQWS_VALIDATION_COMMAND, [
"validate_nfqws2_strategy_json",
normalized,
])
.then((result) => {
const payload = JSON.parse(
(result && result.stdout ? result.stdout : "{}").trim() || "{}",
);
return cacheNfqws2RemoteValidation(normalized, {
valid: payload.valid === true,
message: payload.message || "",
needle: payload.needle || "",
needles: Array.isArray(payload.needles)
? payload.needles.filter(Boolean)
: payload.needle
? [payload.needle]
: [],
});
})
.catch((error) =>
cacheNfqws2RemoteValidation(
normalized,
buildNfqws2RemoteValidationFallback(error),
),
)
.finally(() => {
nfqws2RemoteValidationInflight.delete(normalized);
});
nfqws2RemoteValidationInflight.set(normalized, validationTask);
return validationTask;
}
function getNfqws2ForbiddenTokenInfo(token, index, nextToken) {
const configFileMessage = _(
"External nfqws2 config files bypass Tachyon queue management and explicit validation.",
);
const hostSelectionMessage = _(
"Resource selection by hostname inside nfqws2 is not supported here; sing-box selects resources before NFQUEUE.",
);
const ipSelectionMessage = _(
"Resource selection by IP or CIDR inside nfqws2 is not supported here; sing-box selects resources before NFQUEUE.",
);
const placeholderMessage = _(
"Zapret2 hostlist templates are not supported here because Tachyon does not expand them for per-rule NFQWS2 strategies.",
);
const queueMessage = _(
"The NFQUEUE number is assigned by Tachyon for each rule and must not be overridden here.",
);
const fwmarkMessage = _(
"The desync fwmark is managed by Tachyon for loop prevention and must not be overridden here.",
);
const fuzzMessage = _(
"Fuzzing is not supported here because Tachyon needs deterministic runtime validation.",
);
const interceptMessage = _(
"Disabling interception is incompatible with action=zapret2 because Tachyon sends matched traffic through NFQUEUE.",
);
const daemonMessage = _(
"Tachyon manages the nfqws2 process lifecycle itself, so daemon mode is not allowed here.",
);
const dryRunMessage = _(
"This field must start a working nfqws2 strategy; --dry-run exits immediately and is not allowed here.",
);
const versionMessage = _(
"This field must start a working nfqws2 strategy; --version exits immediately and is not allowed here.",
);
if (index === 0 && (token.startsWith("@") || token.startsWith("$"))) {
return {
reason: configFileMessage,
captureNextValue: false,
};
}
if (token === "<HOSTLIST>" || token === "<HOSTLIST_NOAUTO>") {
return {
reason: placeholderMessage,
captureNextValue: false,
};
}
if (
token === "--hostlist" ||
token.startsWith("--hostlist=") ||
token === "--hostlist-domains" ||
token.startsWith("--hostlist-domains=") ||
token === "--hostlist-exclude" ||
token.startsWith("--hostlist-exclude=") ||
token === "--hostlist-exclude-domains" ||
token.startsWith("--hostlist-exclude-domains=") ||
token === "--hostlist-auto" ||
token.startsWith("--hostlist-auto=") ||
token === "--hostlist-auto-fail-threshold" ||
token.startsWith("--hostlist-auto-fail-threshold=") ||
token === "--hostlist-auto-fail-time" ||
token.startsWith("--hostlist-auto-fail-time=") ||
token === "--hostlist-auto-retrans-threshold" ||
token.startsWith("--hostlist-auto-retrans-threshold=") ||
token === "--hostlist-auto-debug" ||
token.startsWith("--hostlist-auto-debug=") ||
token === "--hostlist-auto-retrans-reset" ||
token.startsWith("--hostlist-auto-retrans-reset=")
) {
return {
reason: hostSelectionMessage,
captureNextValue: !token.includes("="),
};
}
if (
token === "--ipset" ||
token.startsWith("--ipset=") ||
token === "--ipset-ip" ||
token.startsWith("--ipset-ip=") ||
token === "--ipset-exclude" ||
token.startsWith("--ipset-exclude=") ||
token === "--ipset-exclude-ip" ||
token.startsWith("--ipset-exclude-ip=")
) {
return {
reason: ipSelectionMessage,
captureNextValue: !token.includes("="),
};
}
if (token === "--qnum" || token.startsWith("--qnum=")) {
return {
reason: queueMessage,
captureNextValue: !token.includes("="),
};
}
if (
token === "--fwmark" ||
token.startsWith("--fwmark=") ||
token === "--dpi-desync-fwmark" ||
token.startsWith("--dpi-desync-fwmark=")
) {
return {
reason: fwmarkMessage,
captureNextValue: !token.includes("="),
};
}
if (token === "--fuzz" || token.startsWith("--fuzz=")) {
return {
reason: fuzzMessage,
captureNextValue: !token.includes("="),
};
}
if (
token === "--intercept=0" ||
token === "--intercept=false" ||
token === "--intercept=no" ||
(token === "--intercept" && nextToken && nextToken.value === "0")
) {
return {
reason: interceptMessage,
captureNextValue: token === "--intercept",
};
}
if (token === "--daemon") {
return {
reason: daemonMessage,
captureNextValue: false,
};
}
if (token === "--dry-run") {
return {
reason: dryRunMessage,
captureNextValue: false,
};
}
if (token === "--version") {
return {
reason: versionMessage,
captureNextValue: false,
};
}
return null;
}
function buildNfqws2LocalAnalysis(value) {
const text = value ? `${value}` : "";
if (!text.trim().length) {
return {
valid: false,
message: _("NFQWS2 strategy cannot be empty"),
annotations: [],
};
}
const tokens = parseNfqwsRuntimeTokens(text);
const annotationMap = new Map();
const errors = [];
for (let index = 0; index < tokens.length; ) {
const token = tokens[index];
const bareToken = token.value.includes("=")
? token.value.slice(0, token.value.indexOf("="))
: token.value;
const nextToken = tokens[index + 1] || null;
const forbidden = getNfqws2ForbiddenTokenInfo(
token.value,
index,
nextToken,
);
if (forbidden) {
addAnnotationIssue(annotationMap, token, forbidden.reason);
let displayToken = token.value;
if (
forbidden.captureNextValue &&
nextToken &&
!nextToken.value.startsWith("--")
) {
addAnnotationIssue(annotationMap, nextToken, forbidden.reason);
displayToken = `${displayToken} ${nextToken.value}`;
index += 2;
} else {
index += 1;
}
errors.push(`${displayToken}: ${forbidden.reason}`);
continue;
}
if (!token.value.startsWith("--")) {
const reason = _(
"Unexpected standalone token. Use explicit flags such as --name or --name=value.",
);
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
index += 1;
continue;
}
const mode = getNfqws2OptionArgumentMode(bareToken);
if (mode === "unknown") {
const reason = _("Unknown NFQWS2 flag.");
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
index += 1;
continue;
}
if (mode === "none") {
if (token.value.includes("=")) {
const reason = _("This flag does not accept a value.");
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
}
index += 1;
continue;
}
if (mode === "optional") {
if (
nextToken &&
!token.value.includes("=") &&
!nextToken.value.startsWith("--")
) {
const reason = _(
"Optional values must be attached with '=' here; a separate token would be ignored by nfqws2.",
);
addAnnotationIssue(annotationMap, token, reason);
addAnnotationIssue(annotationMap, nextToken, reason);
errors.push(`${token.value} ${nextToken.value}: ${reason}`);
index += 2;
} else {
index += 1;
}
continue;
}
if (!token.value.includes("=")) {
if (!nextToken || nextToken.value.startsWith("--")) {
const reason = _("This option requires a value.");
addAnnotationIssue(annotationMap, token, reason);
errors.push(`${token.value}: ${reason}`);
index += 1;
continue;
}
index += 2;
continue;
}
index += 1;
}
if (!errors.length) {
return { valid: true, message: "", annotations: [] };
}
return {
valid: false,
message: [getValidationHeaderText(), ...errors].join("\n"),
annotations: finalizeAnnotations(annotationMap),
};
}
function analyzeNfqws2Strategy(value) {
const localAnalysis = buildNfqws2LocalAnalysis(value);
if (!localAnalysis.valid) {
return localAnalysis;
}
const remoteValidation = getCachedNfqws2RemoteValidation(value);
if (!remoteValidation || remoteValidation.valid) {
return localAnalysis;
}
const text = value ? `${value}` : "";
const tokens = parseNfqwsRuntimeTokens(text);
const annotationMap = new Map();
localAnalysis.annotations.forEach((annotation) =>
addAnnotationIssue(annotationMap, annotation, annotation.message),
);
addNfqwsRemoteValidationAnnotations(annotationMap, tokens, remoteValidation);
return {
valid: false,
message: [getValidationHeaderText(), remoteValidation.message].join("\n"),
annotations: finalizeAnnotations(annotationMap),
};
}
function normalizeByedpiStrategyWhitespace(value) {
return value ? `${value}`.replace(/\s+/g, " ").trim() : "";
}
function normalizeByedpiStrategyValue(value) {
const normalized = normalizeByedpiStrategyWhitespace(value);
return normalized.length ? normalized : BYEDPI_DEFAULT_CMD_OPTS;
}
function getCachedByedpiRemoteValidation(value) {
const normalized = normalizeByedpiStrategyValue(value);
return normalized.length
? byedpiRemoteValidationCache.get(normalized) || null
: null;
}
function cacheByedpiRemoteValidation(value, result) {
const normalized = normalizeByedpiStrategyValue(value);
if (!normalized.length) {
return result;
}
const cached = {
valid: result && result.valid === true,
message: result && result.message ? `${result.message}` : "",
needle: result && result.needle ? `${result.needle}` : "",
needles:
result && Array.isArray(result.needles)
? result.needles.filter(Boolean).map((item) => `${item}`)
: result && result.needle
? [`${result.needle}`]
: [],
};
byedpiRemoteValidationCache.set(normalized, cached);
return cached;
}
function buildByedpiRemoteValidationFallback(error) {
const message =
error && error.message
? `${error.message}`
: _("Unable to validate the ByeDPI strategy through the backend parser.");
return {
valid: false,
message: _("Backend validation failed: %s").format(message),
needle: "",
needles: [],
};
}
function validateByedpiStrategyRemotely(value) {
const normalized = normalizeByedpiStrategyValue(value);
if (!normalized.length) {
return Promise.resolve({
valid: true,
message: "",
needle: "",
needles: [],
});
}
if (byedpiRemoteValidationCache.has(normalized)) {
return Promise.resolve(byedpiRemoteValidationCache.get(normalized));
}
if (byedpiRemoteValidationInflight.has(normalized)) {
return byedpiRemoteValidationInflight.get(normalized);
}
const validationTask = fs
.exec(NFQWS_VALIDATION_COMMAND, [
"validate_byedpi_strategy_json",
normalized,
])
.then((result) => {
const payload = JSON.parse(
(result && result.stdout ? result.stdout : "{}").trim() || "{}",
);
return cacheByedpiRemoteValidation(normalized, {
valid: payload.valid === true,
message: payload.message || "",
needle: payload.needle || "",
needles: Array.isArray(payload.needles)
? payload.needles.filter(Boolean)
: payload.needle
? [payload.needle]
: [],
});
})
.catch((error) =>
cacheByedpiRemoteValidation(
normalized,
buildByedpiRemoteValidationFallback(error),
),
)
.finally(() => {
byedpiRemoteValidationInflight.delete(normalized);
});
byedpiRemoteValidationInflight.set(normalized, validationTask);
return validationTask;
}
function getByedpiShortOptionName(token) {
return token.length > 2 ? token.slice(0, 2) : token;
}
function byedpiTokenLooksLikeOption(token) {
return /^--.+/.test(token) || /^-[A-Za-z].*/.test(token);
}
function getByedpiControlledTokenInfo(token) {
const listenMessage = _(
"ByeDPI listen address and port are assigned by Tachyon and must not be set in the strategy.",
);
const transparentMessage = _(
"Transparent proxy mode is incompatible with action=byedpi because Tachyon connects to ciadpi through SOCKS.",
);
const daemonMessage = _(
"Tachyon manages the ciadpi process lifecycle itself, so daemon mode is not allowed here.",
);
const pidfileMessage = _(
"Tachyon manages ciadpi pid files itself, so pidfile options are not allowed here.",
);
const exitMessage = _(
"This field must start a working ciadpi strategy; help/version options exit immediately and are not allowed here.",
);
if (
token === "--ip" ||
token.startsWith("--ip=") ||
token === "-i" ||
/^-i.+/.test(token) ||
token === "--port" ||
token.startsWith("--port=") ||
token === "-p" ||
/^-p.+/.test(token)
) {
return {
reason: listenMessage,
captureNextValue:
token === "--ip" ||
token === "-i" ||
token === "--port" ||
token === "-p",
};
}
if (token === "--transparent" || token === "-E" || /^-E.+/.test(token)) {
return {
reason: transparentMessage,
captureNextValue: false,
};
}
if (token === "--daemon" || token === "-D" || /^-D.+/.test(token)) {
return {
reason: daemonMessage,
captureNextValue: false,
};
}
if (
token === "--pidfile" ||
token.startsWith("--pidfile=") ||
token === "-w" ||
/^-w.+/.test(token)
) {
return {
reason: pidfileMessage,
captureNextValue: token === "--pidfile" || token === "-w",
};
}
if (
token === "--help" ||
token === "-h" ||
/^-h.+/.test(token) ||
token === "--version" ||
token === "-v" ||
/^-v.+/.test(token)
) {
return {
reason: exitMessage,
captureNextValue: false,
};
}
return null;
}
function validateByedpiStrategyToken(token, nextToken) {
const controlled = getByedpiControlledTokenInfo(token);
if (controlled) {
return {
valid: false,
reason: controlled.reason,
captureNextValue: controlled.captureNextValue,
};
}
if (/^--[^=]+=/.test(</