diff --git a/fe-app-podkop/src/validators/tests/validateVlessUrl.test.js b/fe-app-podkop/src/validators/tests/validateVlessUrl.test.js new file mode 100644 index 0000000..114cb22 --- /dev/null +++ b/fe-app-podkop/src/validators/tests/validateVlessUrl.test.js @@ -0,0 +1,100 @@ +import { describe, it, expect } from 'vitest'; +import { validateVlessUrl } from '../validateVlessUrl'; + +const validUrls = [ + // TCP + [ + 'tcp + none', + 'vless://94792286-7bbe-4f33-8b36-18d1bbf70723@127.0.0.1:34520?type=tcp&encryption=none&security=none#vless-tcp-none', + ], + [ + 'tcp + reality', + 'vless://e95163dc-905e-480a-afe5-20b146288679@127.0.0.1:16399?type=tcp&encryption=none&security=reality&pbk=tqhSkeDR6jsqC-BYCnZWBrdL33g705ba8tV5-ZboWTM&fp=chrome&sni=google.com&sid=f6&spx=%2F#vless-tcp-reality', + ], + [ + 'tcp + tls', + 'vless://2e9e8288-060e-4da2-8b9f-a1c81826feb7@127.0.0.1:19316?type=tcp&encryption=none&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#vless-tcp-tls', + ], + // mKCP + [ + 'mKCP + none', + 'vless://72e201d7-7841-4a32-b266-4aa3eb776d51@127.0.0.1:17270?type=kcp&encryption=none&headerType=none&seed=AirziWi4ng&security=none#vless-mKCP', + ], + // WebSocket + [ + 'ws + none', + 'vless://d86daef7-565b-4ecd-a9ee-bac847ad38e6@127.0.0.1:12928?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=none#vless-websocket-none', + ], + [ + 'ws + tls', + 'vless://fe0f0941-09a9-4e46-bc69-e00190d7bb9c@127.0.0.1:10156?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=tls&fp=chrome&sni=google.com#vless-websocket-tls', + ], + // gRPC + [ + 'grpc + none', + 'vless://974b39e3-f7bf-42b9-933c-16699c635e77@127.0.0.1:15633?type=grpc&encryption=none&serviceName=TunService&security=none#vless-gRPC-none', + ], + [ + 'grpc + reality', + 'vless://651e7eca-5152-46f1-baf2-d502e0af7b27@127.0.0.1:28535?type=grpc&encryption=none&serviceName=TunService&security=reality&pbk=nhZ7NiKfcqESa5ZeBFfsq9o18W-OWOAHLln9UmuVXSk&fp=chrome&sni=google.com&sid=11cbaeaa&spx=%2F#vless-gRPC-reality', + ], + // HTTPUpgrade + [ + 'httpupgrade + none', + 'vless://2b98f144-847f-42f7-8798-e1a32d27bdc7@127.0.0.1:47154?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=none#vless-httpupgrade-none', + ], + [ + 'httpupgrade + tls', + 'vless://76dbd0ff-1a35-4f0c-a9ba-3c5890b7dea6@127.0.0.1:50639?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=tls&sni=google.com#vless-httpupgrade-tls', + ], + // XHTTP + [ + 'xhttp + none', + 'vless://c2841505-ec32-4b8d-b6dd-3e19d648c321@127.0.0.1:45507?type=xhttp&encryption=none&path=%2Fxhttppath&host=xhttp&mode=auto&security=none#vless-xhttp', + ], +]; + +const invalidUrls = [ + ['No prefix', 'uuid@host:443?type=tcp&security=tls'], + ['No uuid', 'vless://@127.0.0.1:443?type=tcp&security=tls'], + ['No host', 'vless://uuid@:443?type=tcp&security=tls'], + ['No port', 'vless://uuid@127.0.0.1?type=tcp&security=tls'], + ['Invalid port', 'vless://uuid@127.0.0.1:abc?type=tcp&security=tls'], + ['Missing type', 'vless://uuid@127.0.0.1:443?security=tls'], + ['Missing security', 'vless://uuid@127.0.0.1:443?type=tcp'], + [ + 'reality without pbk', + 'vless://uuid@127.0.0.1:443?type=tcp&security=reality&fp=chrome', + ], + [ + 'reality without fp', + 'vless://uuid@127.0.0.1:443?type=tcp&security=reality&pbk=abc', + ], + [ + 'tcp + reality + unexpected spaces', + 'vless://e95163dc-905e-480a-afe5-20b146288679@127.0.0.1:16399?type=tcp&encryption=none&security=reality&pbk=tqhSkeDR6jsqC-BYCnZWBrdL33g705ba8tV5-ZboWTM&fp=chrome&sni= google.com&sid=f6&spx=%2F#vless-tcp-reality', + ], +]; + +describe('validateVlessUrl', () => { + describe.each(validUrls)('Valid URL: %s', (_desc, url) => { + it(`returns valid=true for "${url}"`, () => { + const res = validateVlessUrl(url); + expect(res.valid).toBe(true); + }); + }); + + describe.each(invalidUrls)('Invalid URL: %s', (_desc, url) => { + it(`returns valid=false for "${url}"`, () => { + const res = validateVlessUrl(url); + expect(res.valid).toBe(false); + }); + }); + + it('detects invalid port range', () => { + const res = validateVlessUrl( + 'vless://uuid@127.0.0.1:99999?type=tcp&security=tls', + ); + expect(res.valid).toBe(false); + }); +}); diff --git a/fe-app-podkop/src/validators/validateShadowsocksUrl.ts b/fe-app-podkop/src/validators/validateShadowsocksUrl.ts index ca9e40e..68081a7 100644 --- a/fe-app-podkop/src/validators/validateShadowsocksUrl.ts +++ b/fe-app-podkop/src/validators/validateShadowsocksUrl.ts @@ -10,6 +10,13 @@ export function validateShadowsocksUrl(url: string): ValidationResult { } 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]; diff --git a/fe-app-podkop/src/validators/validateTrojanUrl.ts b/fe-app-podkop/src/validators/validateTrojanUrl.ts index 49b85b6..f79536c 100644 --- a/fe-app-podkop/src/validators/validateTrojanUrl.ts +++ b/fe-app-podkop/src/validators/validateTrojanUrl.ts @@ -9,6 +9,13 @@ export function validateTrojanUrl(url: string): ValidationResult { }; } + if (!url || /\s/.test(url)) { + return { + valid: false, + message: 'Invalid Trojan URL: must not contain spaces', + }; + } + try { const parsedUrl = new URL(url); diff --git a/fe-app-podkop/src/validators/validateVlessUrl.ts b/fe-app-podkop/src/validators/validateVlessUrl.ts index 22189ab..e74ffa6 100644 --- a/fe-app-podkop/src/validators/validateVlessUrl.ts +++ b/fe-app-podkop/src/validators/validateVlessUrl.ts @@ -2,62 +2,68 @@ import { ValidationResult } from './types'; // TODO refactor current validation and add tests export function validateVlessUrl(url: string): ValidationResult { - if (!url.startsWith('vless://')) { - return { - valid: false, - message: 'Invalid VLESS URL: must start with vless://', - }; - } - try { - const uuid = url.split('/')[2]?.split('@')[0]; + const parsedUrl = new URL(url); - if (!uuid) { + 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 { @@ -94,9 +100,9 @@ export function validateVlessUrl(url: string): ValidationResult { }; } } + + return { valid: true, message: 'Valid' }; } catch (_e) { return { valid: false, message: 'Invalid VLESS URL: parsing failed' }; } - - return { valid: true, message: 'Valid' }; } diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js index 84107e6..6ce0f09 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js @@ -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') + ' github.com/itdoginfo/allow-domains', ); 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'); diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index b3a46ff..ec52014 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -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) {