refactor: change vless validation logic

This commit is contained in:
divocat
2025-10-05 18:13:19 +03:00
parent c5e19a0f2d
commit 341f260fcf
6 changed files with 418 additions and 274 deletions

View File

@@ -12,11 +12,11 @@ function createConfigSection(section) {
let o = s.tab('basic', _('Basic Settings'));
o = s.taboption(
'basic',
form.ListValue,
'mode',
_('Connection Type'),
_('Select between VPN and Proxy connection methods for traffic routing'),
'basic',
form.ListValue,
'mode',
_('Connection Type'),
_('Select between VPN and Proxy connection methods for traffic routing'),
);
o.value('proxy', 'Proxy');
o.value('vpn', 'VPN');
@@ -24,11 +24,11 @@ function createConfigSection(section) {
o.ucisection = s.section;
o = s.taboption(
'basic',
form.ListValue,
'proxy_config_type',
_('Configuration Type'),
_('Select how to configure the proxy'),
'basic',
form.ListValue,
'proxy_config_type',
_('Configuration Type'),
_('Select how to configure the proxy'),
);
o.value('url', _('Connection URL'));
o.value('outbound', _('Outbound Config'));
@@ -38,11 +38,11 @@ function createConfigSection(section) {
o.ucisection = s.section;
o = s.taboption(
'basic',
form.TextValue,
'proxy_string',
_('Proxy Configuration URL'),
'',
'basic',
form.TextValue,
'proxy_string',
_('Proxy Configuration URL'),
'',
);
o.depends('proxy_config_type', 'url');
o.rows = 5;
@@ -52,7 +52,7 @@ function createConfigSection(section) {
o.ucisection = s.section;
o.sectionDescriptions = new Map();
o.placeholder =
'vless://uuid@server:port?type=tcp&security=tls#main\n// backup ss://method:pass@server:port\n// backup2 vless://uuid@server:port?type=grpc&security=reality#alt\n// backup3 trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none';
'vless://uuid@server:port?type=tcp&security=tls#main\n// backup ss://method:pass@server:port\n// backup2 vless://uuid@server:port?type=grpc&security=reality#alt\n// backup3 trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none';
o.renderWidget = function (section_id, option_index, cfgvalue) {
const original = form.TextValue.prototype.renderWidget.apply(this, [
@@ -66,9 +66,9 @@ function createConfigSection(section) {
if (cfgvalue) {
try {
const activeConfig = cfgvalue
.split('\n')
.map((line) => line.trim())
.find((line) => line && !line.startsWith('//'));
.split('\n')
.map((line) => line.trim())
.find((line) => line && !line.startsWith('//'));
if (activeConfig) {
if (activeConfig.includes('#')) {
@@ -76,24 +76,24 @@ function createConfigSection(section) {
if (label && label.trim()) {
const decodedLabel = decodeURIComponent(label);
const descDiv = E(
'div',
{ class: 'cbi-value-description' },
_('Current config: ') + decodedLabel,
'div',
{ class: 'cbi-value-description' },
_('Current config: ') + decodedLabel,
);
container.appendChild(descDiv);
} else {
const descDiv = E(
'div',
{ class: 'cbi-value-description' },
_('Config without description'),
'div',
{ class: 'cbi-value-description' },
_('Config without description'),
);
container.appendChild(descDiv);
}
} else {
const descDiv = E(
'div',
{ class: 'cbi-value-description' },
_('Config without description'),
'div',
{ class: 'cbi-value-description' },
_('Config without description'),
);
container.appendChild(descDiv);
}
@@ -101,19 +101,19 @@ function createConfigSection(section) {
} catch (e) {
console.error('Error parsing config label:', e);
const descDiv = E(
'div',
{ class: 'cbi-value-description' },
_('Config without description'),
'div',
{ class: 'cbi-value-description' },
_('Config without description'),
);
container.appendChild(descDiv);
}
} else {
const defaultDesc = E(
'div',
{ class: 'cbi-value-description' },
_(
'Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs',
),
'div',
{ class: 'cbi-value-description' },
_(
'Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs',
),
);
container.appendChild(defaultDesc);
}
@@ -128,18 +128,25 @@ function createConfigSection(section) {
}
try {
const activeConfig = value
.split('\n')
.map((line) => line.trim())
.find((line) => line && !line.startsWith('//'));
const activeConfigs = value
.split('\n')
.map((line) => line.trim())
.filter((line) => !line.startsWith('//'))
.filter(Boolean);
if (!activeConfig) {
if (!activeConfigs.length) {
return _(
'No active configuration found. At least one non-commented line is required.',
'No active configuration found. One configuration is required.',
);
}
const validation = main.validateProxyUrl(activeConfig);
if (activeConfigs.length > 1) {
return _(
'Multiply active configurations found. Please leave one configuration.',
);
}
const validation = main.validateProxyUrl(activeConfigs[0]);
if (validation.valid) {
return true;
@@ -152,11 +159,11 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.TextValue,
'outbound_json',
_('Outbound Configuration'),
_('Enter complete outbound configuration in JSON format'),
'basic',
form.TextValue,
'outbound_json',
_('Outbound Configuration'),
_('Enter complete outbound configuration in JSON format'),
);
o.depends('proxy_config_type', 'outbound');
o.rows = 10;
@@ -177,10 +184,10 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.DynamicList,
'urltest_proxy_links',
_('URLTest Proxy Links'),
'basic',
form.DynamicList,
'urltest_proxy_links',
_('URLTest Proxy Links'),
);
o.depends('proxy_config_type', 'urltest');
o.placeholder = 'vless://, ss://, trojan:// links';
@@ -201,11 +208,11 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.Flag,
'ss_uot',
_('Shadowsocks UDP over TCP'),
_('Apply for SS2022'),
'basic',
form.Flag,
'ss_uot',
_('Shadowsocks UDP over TCP'),
_('Apply for SS2022'),
);
o.default = '0';
o.depends('mode', 'proxy');
@@ -213,11 +220,11 @@ function createConfigSection(section) {
o.ucisection = s.section;
o = s.taboption(
'basic',
widgets.DeviceSelect,
'interface',
_('Network Interface'),
_('Select network interface for VPN connection'),
'basic',
widgets.DeviceSelect,
'interface',
_('Network Interface'),
_('Select network interface for VPN connection'),
);
o.depends('mode', 'vpn');
o.ucisection = s.section;
@@ -255,17 +262,17 @@ function createConfigSection(section) {
// Reject wireless-related devices
const isWireless =
type === 'wifi' || type === 'wireless' || type.includes('wlan');
type === 'wifi' || type === 'wireless' || type.includes('wlan');
return !isWireless;
};
o = s.taboption(
'basic',
form.Flag,
'domain_resolver_enabled',
_('Domain Resolver'),
_('Enable built-in DNS resolver for domains handled by this section'),
'basic',
form.Flag,
'domain_resolver_enabled',
_('Domain Resolver'),
_('Enable built-in DNS resolver for domains handled by this section'),
);
o.default = '0';
o.rmempty = false;
@@ -273,11 +280,11 @@ function createConfigSection(section) {
o.ucisection = s.section;
o = s.taboption(
'basic',
form.ListValue,
'domain_resolver_dns_type',
_('DNS Protocol Type'),
_('Select the DNS protocol type for the domain resolver'),
'basic',
form.ListValue,
'domain_resolver_dns_type',
_('DNS Protocol Type'),
_('Select the DNS protocol type for the domain resolver'),
);
o.value('doh', _('DNS over HTTPS (DoH)'));
o.value('dot', _('DNS over TLS (DoT)'));
@@ -288,11 +295,11 @@ function createConfigSection(section) {
o.ucisection = s.section;
o = s.taboption(
'basic',
form.Value,
'domain_resolver_dns_server',
_('DNS Server'),
_('Select or enter DNS server address'),
'basic',
form.Value,
'domain_resolver_dns_server',
_('DNS Server'),
_('Select or enter DNS server address'),
);
Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
@@ -312,21 +319,21 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.Flag,
'community_lists_enabled',
_('Community Lists'),
'basic',
form.Flag,
'community_lists_enabled',
_('Community Lists'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'community_lists',
_('Service List'),
_('Select predefined service for routing') +
'basic',
form.DynamicList,
'community_lists',
_('Service List'),
_('Select predefined service for routing') +
' <a href="https://github.com/itdoginfo/allow-domains" target="_blank">github.com/itdoginfo/allow-domains</a>',
);
o.placeholder = 'Service list';
@@ -350,50 +357,50 @@ function createConfigSection(section) {
let notifications = [];
const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter((opt) =>
newValues.includes(opt),
newValues.includes(opt),
);
if (selectedRegionalOptions.length > 1) {
const lastSelected =
selectedRegionalOptions[selectedRegionalOptions.length - 1];
selectedRegionalOptions[selectedRegionalOptions.length - 1];
const removedRegions = selectedRegionalOptions.slice(0, -1);
newValues = newValues.filter(
(v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v),
(v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v),
);
notifications.push(
E('p', { class: 'alert-message warning' }, [
E('strong', {}, _('Regional options cannot be used together')),
E('br'),
_(
'Warning: %s cannot be used together with %s. Previous selections have been removed.',
).format(removedRegions.join(', '), lastSelected),
]),
E('p', { class: 'alert-message warning' }, [
E('strong', {}, _('Regional options cannot be used together')),
E('br'),
_(
'Warning: %s cannot be used together with %s. Previous selections have been removed.',
).format(removedRegions.join(', '), lastSelected),
]),
);
}
if (newValues.includes('russia_inside')) {
const removedServices = newValues.filter(
(v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
(v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
);
if (removedServices.length > 0) {
newValues = newValues.filter((v) =>
main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
);
notifications.push(
E('p', { class: 'alert-message warning' }, [
E('strong', {}, _('Russia inside restrictions')),
E('br'),
_(
'Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.',
).format(
main.ALLOWED_WITH_RUSSIA_INSIDE.map(
(key) => main.DOMAIN_LIST_OPTIONS[key],
)
.filter((label) => label !== 'Russia inside')
.join(', '),
removedServices.join(', '),
),
]),
E('p', { class: 'alert-message warning' }, [
E('strong', {}, _('Russia inside restrictions')),
E('br'),
_(
'Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.',
).format(
main.ALLOWED_WITH_RUSSIA_INSIDE.map(
(key) => main.DOMAIN_LIST_OPTIONS[key],
)
.filter((label) => label !== 'Russia inside')
.join(', '),
removedServices.join(', '),
),
]),
);
}
}
@@ -403,7 +410,7 @@ function createConfigSection(section) {
}
notifications.forEach((notification) =>
ui.addNotification(null, notification),
ui.addNotification(null, notification),
);
lastValues = newValues;
} catch (e) {
@@ -414,11 +421,11 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.ListValue,
'user_domain_list_type',
_('User Domain List Type'),
_('Select how to add your custom domains'),
'basic',
form.ListValue,
'user_domain_list_type',
_('User Domain List Type'),
_('Select how to add your custom domains'),
);
o.value('disabled', _('Disabled'));
o.value('dynamic', _('Dynamic List'));
@@ -428,13 +435,13 @@ function createConfigSection(section) {
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'user_domains',
_('User Domains'),
_(
'Enter domain names without protocols (example: sub.example.com or example.com)',
),
'basic',
form.DynamicList,
'user_domains',
_('User Domains'),
_(
'Enter domain names without protocols (example: sub.example.com or example.com)',
),
);
o.placeholder = 'Domains list';
o.depends('user_domain_list_type', 'dynamic');
@@ -456,16 +463,16 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.TextValue,
'user_domains_text',
_('User Domains List'),
_(
'Enter domain names separated by comma, space or newline. You can add comments after //',
),
'basic',
form.TextValue,
'user_domains_text',
_('User Domains List'),
_(
'Enter domain names separated by comma, space or newline. You can add comments after //',
),
);
o.placeholder =
'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains';
'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains';
o.depends('user_domain_list_type', 'text');
o.rows = 8;
o.rmempty = false;
@@ -480,7 +487,7 @@ function createConfigSection(section) {
if (!domains.length) {
return _(
'At least one valid domain must be specified. Comments-only content is not allowed.',
'At least one valid domain must be specified. Comments-only content is not allowed.',
);
}
@@ -488,8 +495,8 @@ function createConfigSection(section) {
if (!valid) {
const errors = results
.filter((validation) => !validation.valid) // Leave only failed validations
.map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors
.filter((validation) => !validation.valid) // Leave only failed validations
.map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors
return [_('Validation errors:'), ...errors].join('\n');
}
@@ -498,22 +505,22 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.Flag,
'local_domain_lists_enabled',
_('Local Domain Lists'),
_('Use the list from the router filesystem'),
'basic',
form.Flag,
'local_domain_lists_enabled',
_('Local Domain Lists'),
_('Use the list from the router filesystem'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'local_domain_lists',
_('Local Domain List Paths'),
_('Enter the list file path'),
'basic',
form.DynamicList,
'local_domain_lists',
_('Local Domain List Paths'),
_('Enter the list file path'),
);
o.placeholder = '/path/file.lst';
o.depends('local_domain_lists_enabled', '1');
@@ -535,22 +542,22 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.Flag,
'remote_domain_lists_enabled',
_('Remote Domain Lists'),
_('Download and use domain lists from remote URLs'),
'basic',
form.Flag,
'remote_domain_lists_enabled',
_('Remote Domain Lists'),
_('Download and use domain lists from remote URLs'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'remote_domain_lists',
_('Remote Domain URLs'),
_('Enter full URLs starting with http:// or https://'),
'basic',
form.DynamicList,
'remote_domain_lists',
_('Remote Domain URLs'),
_('Enter full URLs starting with http:// or https://'),
);
o.placeholder = 'URL';
o.depends('remote_domain_lists_enabled', '1');
@@ -572,22 +579,22 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.Flag,
'local_subnet_lists_enabled',
_('Local Subnet Lists'),
_('Use the list from the router filesystem'),
'basic',
form.Flag,
'local_subnet_lists_enabled',
_('Local Subnet Lists'),
_('Use the list from the router filesystem'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'local_subnet_lists',
_('Local Subnet List Paths'),
_('Enter the list file path'),
'basic',
form.DynamicList,
'local_subnet_lists',
_('Local Subnet List Paths'),
_('Enter the list file path'),
);
o.placeholder = '/path/file.lst';
o.depends('local_subnet_lists_enabled', '1');
@@ -609,11 +616,11 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.ListValue,
'user_subnet_list_type',
_('User Subnet List Type'),
_('Select how to add your custom subnets'),
'basic',
form.ListValue,
'user_subnet_list_type',
_('User Subnet List Type'),
_('Select how to add your custom subnets'),
);
o.value('disabled', _('Disabled'));
o.value('dynamic', _('Dynamic List'));
@@ -623,13 +630,13 @@ function createConfigSection(section) {
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'user_subnets',
_('User Subnets'),
_(
'Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses',
),
'basic',
form.DynamicList,
'user_subnets',
_('User Subnets'),
_(
'Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses',
),
);
o.placeholder = 'IP or subnet';
o.depends('user_subnet_list_type', 'dynamic');
@@ -651,16 +658,16 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.TextValue,
'user_subnets_text',
_('User Subnets List'),
_(
'Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //',
),
'basic',
form.TextValue,
'user_subnets_text',
_('User Subnets List'),
_(
'Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //',
),
);
o.placeholder =
'103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9';
'103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9';
o.depends('user_subnet_list_type', 'text');
o.rows = 10;
o.rmempty = false;
@@ -675,7 +682,7 @@ function createConfigSection(section) {
if (!subnets.length) {
return _(
'At least one valid subnet or IP must be specified. Comments-only content is not allowed.',
'At least one valid subnet or IP must be specified. Comments-only content is not allowed.',
);
}
@@ -683,8 +690,8 @@ function createConfigSection(section) {
if (!valid) {
const errors = results
.filter((validation) => !validation.valid) // Leave only failed validations
.map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors
.filter((validation) => !validation.valid) // Leave only failed validations
.map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors
return [_('Validation errors:'), ...errors].join('\n');
}
@@ -693,22 +700,22 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.Flag,
'remote_subnet_lists_enabled',
_('Remote Subnet Lists'),
_('Download and use subnet lists from remote URLs'),
'basic',
form.Flag,
'remote_subnet_lists_enabled',
_('Remote Subnet Lists'),
_('Download and use subnet lists from remote URLs'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'remote_subnet_lists',
_('Remote Subnet URLs'),
_('Enter full URLs starting with http:// or https://'),
'basic',
form.DynamicList,
'remote_subnet_lists',
_('Remote Subnet URLs'),
_('Enter full URLs starting with http:// or https://'),
);
o.placeholder = 'URL';
o.depends('remote_subnet_lists_enabled', '1');
@@ -730,24 +737,24 @@ function createConfigSection(section) {
};
o = s.taboption(
'basic',
form.Flag,
'all_traffic_from_ip_enabled',
_('IP for full redirection'),
_(
'Specify local IP addresses whose traffic will always use the configured route',
),
'basic',
form.Flag,
'all_traffic_from_ip_enabled',
_('IP for full redirection'),
_(
'Specify local IP addresses whose traffic will always use the configured route',
),
);
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'all_traffic_ip',
_('Local IPs'),
_('Enter valid IPv4 addresses'),
'basic',
form.DynamicList,
'all_traffic_ip',
_('Local IPs'),
_('Enter valid IPv4 addresses'),
);
o.placeholder = 'IP';
o.depends('all_traffic_from_ip_enabled', '1');

View File

@@ -128,6 +128,12 @@ function validateShadowsocksUrl(url) {
};
}
try {
if (!url || /\s/.test(url)) {
return {
valid: false,
message: "Invalid Shadowsocks URL: must not contain spaces"
};
}
const mainPart = url.includes("?") ? url.split("?")[0] : url.split("#")[0];
const encryptedPart = mainPart.split("/")[2]?.split("@")[0];
if (!encryptedPart) {
@@ -185,49 +191,54 @@ function validateShadowsocksUrl(url) {
// src/validators/validateVlessUrl.ts
function validateVlessUrl(url) {
if (!url.startsWith("vless://")) {
return {
valid: false,
message: "Invalid VLESS URL: must start with vless://"
};
}
try {
const uuid = url.split("/")[2]?.split("@")[0];
if (!uuid) {
const parsedUrl = new URL(url);
if (!url || /\s/.test(url)) {
return {
valid: false,
message: "Invalid VLESS URL: must not contain spaces"
};
}
if (parsedUrl.protocol !== "vless:") {
return {
valid: false,
message: "Invalid VLESS URL: must start with vless://"
};
}
if (!parsedUrl.username) {
return { valid: false, message: "Invalid VLESS URL: missing UUID" };
}
const serverPart = url.split("@")[1];
if (!serverPart) {
return {
valid: false,
message: "Invalid VLESS URL: missing server address"
};
}
const [server, portAndRest] = serverPart.split(":");
if (!server) {
if (!parsedUrl.hostname) {
return { valid: false, message: "Invalid VLESS URL: missing server" };
}
const port = portAndRest ? portAndRest.split(/[/?#]/)[0] : null;
if (!port) {
if (!parsedUrl.port) {
return { valid: false, message: "Invalid VLESS URL: missing port" };
}
const portNum = parseInt(port, 10);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
if (isNaN(+parsedUrl.port) || +parsedUrl.port < 1 || +parsedUrl.port > 65535) {
return {
valid: false,
message: "Invalid port number. Must be between 1 and 65535"
message: "Invalid VLESS URL: invalid port number. Must be between 1 and 65535"
};
}
const queryString = url.split("?")[1];
if (!queryString) {
if (!parsedUrl.search) {
return {
valid: false,
message: "Invalid VLESS URL: missing query parameters"
};
}
const params = new URLSearchParams(queryString.split("#")[0]);
const params = new URLSearchParams(parsedUrl.search);
const type = params.get("type");
const validTypes = ["tcp", "raw", "udp", "grpc", "http", "ws"];
const validTypes = [
"tcp",
"raw",
"udp",
"grpc",
"http",
"httpupgrade",
"xhttp",
"ws",
"kcp"
];
if (!type || !validTypes.includes(type)) {
return {
valid: false,
@@ -256,10 +267,10 @@ function validateVlessUrl(url) {
};
}
}
return { valid: true, message: "Valid" };
} catch (_e) {
return { valid: false, message: "Invalid VLESS URL: parsing failed" };
}
return { valid: true, message: "Valid" };
}
// src/validators/validateOutboundJson.ts
@@ -286,6 +297,12 @@ function validateTrojanUrl(url) {
message: "Invalid Trojan URL: must start with trojan://"
};
}
if (!url || /\s/.test(url)) {
return {
valid: false,
message: "Invalid Trojan URL: must not contain spaces"
};
}
try {
const parsedUrl = new URL(url);
if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) {