From 6df7c8abf85d08d24103d88a0fa2ae028fd708c0 Mon Sep 17 00:00:00 2001 From: Ivan K Date: Tue, 18 Feb 2025 17:18:34 +0300 Subject: [PATCH] feat: add URL validation for Shadowsocks and VLESS configurations from examples --- .../resources/view/podkop/podkop.js | 139 +++++++++++++++++- 1 file changed, 138 insertions(+), 1 deletion(-) diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js index 60a2852..ff6049b 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js @@ -43,6 +43,144 @@ return view.extend({ o.depends('proxy_config_type', 'url'); o.rows = 5; o.ucisection = 'main'; + o.validate = function (section_id, value) { + if (!value || value.length === 0) { + return true; + } + + try { + // Check if it's a valid URL format + if (!value.startsWith('vless://') && !value.startsWith('ss://')) { + return _('URL must start with vless:// or ss://'); + } + + // For Shadowsocks + if (value.startsWith('ss://')) { + let encrypted_part; + try { + // Split URL properly handling both old and new formats + let mainPart = value.includes('?') ? value.split('?')[0] : value.split('#')[0]; + encrypted_part = mainPart.split('/')[2].split('@')[0]; + + // Try base64 decode first (for old format) + try { + let decoded = atob(encrypted_part); + if (!decoded.includes(':')) { + // Not old format, check if it's 2022 format + if (!encrypted_part.includes(':') && !encrypted_part.includes('-')) { + return _('Invalid Shadowsocks URL format: missing method and password separator ":"'); + } + } + } catch (e) { + // If base64 decode fails, check if it's 2022 format + if (!encrypted_part.includes(':') && !encrypted_part.includes('-')) { + return _('Invalid Shadowsocks URL format: missing method and password separator ":"'); + } + } + } catch (e) { + return _('Invalid Shadowsocks URL format'); + } + + // Check server and port + try { + let serverPart = value.split('@')[1]; + if (!serverPart) { + return _('Invalid Shadowsocks URL: missing server address'); + } + let [server, portAndRest] = serverPart.split(':'); + if (!server) { + return _('Invalid Shadowsocks URL: missing server'); + } + let port = portAndRest ? portAndRest.split(/[?#]/)[0] : null; + if (!port) { + return _('Invalid Shadowsocks URL: missing port'); + } + let portNum = parseInt(port); + if (isNaN(portNum) || portNum < 1 || portNum > 65535) { + return _('Invalid port number. Must be between 1 and 65535'); + } + } catch (e) { + return _('Invalid Shadowsocks URL: missing or invalid server/port format'); + } + } + + // For VLESS + if (value.startsWith('vless://')) { + // Check UUID + let uuid = value.split('/')[2].split('@')[0]; + if (!uuid || uuid.length === 0) { + return _('Invalid VLESS URL: missing UUID'); + } + + // Check server and port + try { + let serverPart = value.split('@')[1]; + if (!serverPart) { + return _('Invalid VLESS URL: missing server address'); + } + let [server, portAndRest] = serverPart.split(':'); + if (!server) { + return _('Invalid VLESS URL: missing server'); + } + // Handle cases where port might be followed by / or ? or # + let port = portAndRest ? portAndRest.split(/[/?#]/)[0] : null; + if (!port && port !== '') { // Allow empty port for specific cases + return _('Invalid VLESS URL: missing port'); + } + if (port !== '') { // Only validate port if it's not empty + let portNum = parseInt(port); + if (isNaN(portNum) || portNum < 1 || portNum > 65535) { + return _('Invalid port number. Must be between 1 and 65535'); + } + } + } catch (e) { + return _('Invalid VLESS URL: missing or invalid server/port format'); + } + + // Parse query parameters + let queryString = value.split('?')[1]; + if (!queryString) { + return _('Invalid VLESS URL: missing query parameters'); + } + + let params = new URLSearchParams(queryString.split('#')[0]); + + // Check type parameter + let type = params.get('type'); + if (!type) { + return _('Invalid VLESS URL: missing type parameter'); + } + + // Check security parameter + let security = params.get('security'); + if (!security) { + return _('Invalid VLESS URL: missing security parameter'); + } + + // If security is "reality", check required reality parameters + if (security === 'reality') { + if (!params.get('pbk')) { + return _('Invalid VLESS URL: missing pbk parameter for reality security'); + } + if (!params.get('fp')) { + return _('Invalid VLESS URL: missing fp parameter for reality security'); + } + } + + // If security is "tls", check required TLS parameters + if (security === 'tls') { + if (!params.get('sni') && type !== 'tcp') { + return _('Invalid VLESS URL: missing sni parameter for tls security'); + } + } + } + + return true; + } catch (e) { + console.error('Validation error:', e); + return _('Invalid URL format: ' + e.message); + } + }; o = s.taboption('basic', form.TextValue, 'outbound_json', _('Outbound Configuration'), _('Enter complete outbound configuration in JSON format')); o.depends('proxy_config_type', 'outbound'); @@ -1215,7 +1353,6 @@ return view.extend({ return _('Invalid format. Use format: X.X.X.X or X.X.X.X/Y'); } - // Разбираем IP и маску const [ip, cidr] = value.split('/'); const ipParts = ip.split('.');