diff --git a/fe-app-podkop/src/podkop/services/core.service.ts b/fe-app-podkop/src/podkop/services/core.service.ts index b2d44b3..79b63db 100644 --- a/fe-app-podkop/src/podkop/services/core.service.ts +++ b/fe-app-podkop/src/podkop/services/core.service.ts @@ -30,7 +30,10 @@ export function coreService() { { intervalMs: 3000, onNewLog: (line) => { - if (line.toLowerCase().includes('[error]') || line.toLowerCase().includes('[fatal]')) { + if ( + line.toLowerCase().includes('[error]') || + line.toLowerCase().includes('[fatal]') + ) { ui.addNotification('Podkop Error', E('div', {}, line), 'error'); } }, diff --git a/fe-app-podkop/src/validators/index.ts b/fe-app-podkop/src/validators/index.ts index 88e6b03..9bdad27 100644 --- a/fe-app-podkop/src/validators/index.ts +++ b/fe-app-podkop/src/validators/index.ts @@ -10,3 +10,4 @@ export * from './validateVlessUrl'; export * from './validateOutboundJson'; export * from './validateTrojanUrl'; export * from './validateProxyUrl'; +export * from './validateSocksUrl'; diff --git a/fe-app-podkop/src/validators/tests/validateSocksUrl.test.js b/fe-app-podkop/src/validators/tests/validateSocksUrl.test.js new file mode 100644 index 0000000..e9c4da4 --- /dev/null +++ b/fe-app-podkop/src/validators/tests/validateSocksUrl.test.js @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; +import { validateSocksUrl } from '../validateSocksUrl'; + +const validUrls = [ + ['socks4 basic', 'socks4://127.0.0.1:1080'], + ['socks4a basic', 'socks4a://127.0.0.1:1080'], + ['socks5 basic', 'socks5://127.0.0.1:1080'], + ['socks5 with username', 'socks5://user@127.0.0.1:1080'], + ['socks5 with username/password', 'socks5://user:pass@127.0.0.1:1080'], + ['socks5 with domain', 'socks5://user:pass@my.proxy.com:1080'], + ['socks5 with dash in domain', 'socks5://user:pass@fast-proxy.net:8080'], + ['socks5 with uppercase domain', 'socks5://USER:PASSWORD@Example.COM:1080'], +]; + +const invalidUrls = [ + ['no prefix', '127.0.0.1:1080'], + ['wrong prefix', 'http://127.0.0.1:1080'], + ['missing host', 'socks5://user:pass@:1080'], + ['missing port', 'socks5://127.0.0.1'], + ['invalid port (non-numeric)', 'socks5://127.0.0.1:abc'], + ['invalid port (too high)', 'socks5://127.0.0.1:99999'], + ['space in url', 'socks5://127.0. 0.1:1080'], + ['missing username when auth provided', 'socks5://:pass@127.0.0.1:1080'], + ['invalid domain chars', 'socks5://user:pass@exa_mple.com:1080'], + ['extra symbol', 'socks5:///127.0.0.1:1080'], +]; + +describe('validateSocksUrl', () => { + describe.each(validUrls)('Valid URL: %s', (_desc, url) => { + it(`returns valid=true for "${url}"`, () => { + const res = validateSocksUrl(url); + expect(res.valid).toBe(true); + }); + }); + + describe.each(invalidUrls)('Invalid URL: %s', (_desc, url) => { + it(`returns valid=false for "${url}"`, () => { + const res = validateSocksUrl(url); + expect(res.valid).toBe(false); + }); + }); + + it('detects invalid port range (0)', () => { + const res = validateSocksUrl('socks5://127.0.0.1:0'); + expect(res.valid).toBe(false); + }); + + it('detects invalid port range (65536)', () => { + const res = validateSocksUrl('socks5://127.0.0.1:65536'); + expect(res.valid).toBe(false); + }); +}); diff --git a/fe-app-podkop/src/validators/validateProxyUrl.ts b/fe-app-podkop/src/validators/validateProxyUrl.ts index ec3fe47..15a3003 100644 --- a/fe-app-podkop/src/validators/validateProxyUrl.ts +++ b/fe-app-podkop/src/validators/validateProxyUrl.ts @@ -2,6 +2,7 @@ import { ValidationResult } from './types'; import { validateShadowsocksUrl } from './validateShadowsocksUrl'; import { validateVlessUrl } from './validateVlessUrl'; import { validateTrojanUrl } from './validateTrojanUrl'; +import { validateSocksUrl } from './validateSocksUrl'; // TODO refactor current validation and add tests export function validateProxyUrl(url: string): ValidationResult { @@ -17,8 +18,14 @@ export function validateProxyUrl(url: string): ValidationResult { return validateTrojanUrl(url); } + if (/^socks(4|4a|5):\/\//.test(url)) { + return validateSocksUrl(url); + } + return { valid: false, - message: _('URL must start with vless:// or ss:// or trojan://'), + message: _( + 'URL must start with vless://, ss://, trojan://, or socks4/5://', + ), }; } diff --git a/fe-app-podkop/src/validators/validateSocksUrl.ts b/fe-app-podkop/src/validators/validateSocksUrl.ts new file mode 100644 index 0000000..b59ae6c --- /dev/null +++ b/fe-app-podkop/src/validators/validateSocksUrl.ts @@ -0,0 +1,81 @@ +import { ValidationResult } from './types'; +import { validateDomain } from './validateDomain'; +import { validateIPV4 } from './validateIp'; + +export function validateSocksUrl(url: string): ValidationResult { + try { + if (!/^socks(4|4a|5):\/\//.test(url)) { + return { + valid: false, + message: _( + 'Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://', + ), + }; + } + + if (!url || /\s/.test(url)) { + return { + valid: false, + message: _('Invalid SOCKS URL: must not contain spaces'), + }; + } + + const body = url.replace(/^socks(4|4a|5):\/\//, ''); + const [authAndHost] = body.split('#'); // отбрасываем hash, если есть + const [credentials, hostPortPart] = authAndHost.includes('@') + ? authAndHost.split('@') + : [null, authAndHost]; + + if (credentials) { + const [username, _password] = credentials.split(':'); + if (!username) { + return { + valid: false, + message: _('Invalid SOCKS URL: missing username'), + }; + } + } + + if (!hostPortPart) { + return { + valid: false, + message: _('Invalid SOCKS URL: missing host and port'), + }; + } + + const [host, port] = hostPortPart.split(':'); + + if (!host) { + return { + valid: false, + message: _('Invalid SOCKS URL: missing hostname or IP'), + }; + } + + if (!port) { + return { valid: false, message: _('Invalid SOCKS URL: missing port') }; + } + + const portNum = Number(port); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { + return { + valid: false, + message: _('Invalid SOCKS URL: invalid port number'), + }; + } + + const ipv4Result = validateIPV4(host); + const domainResult = validateDomain(host); + + if (!ipv4Result.valid && !domainResult.valid) { + return { + valid: false, + message: _('Invalid SOCKS URL: invalid host format'), + }; + } + } catch (_e) { + return { valid: false, message: _('Invalid SOCKS URL: parsing failed') }; + } + + return { valid: true, message: _('Valid') }; +} 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 e0d6800..9f60cd7 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 @@ -383,6 +383,72 @@ function validateTrojanUrl(url) { return { valid: true, message: _("Valid") }; } +// src/validators/validateSocksUrl.ts +function validateSocksUrl(url) { + try { + if (!/^socks(4|4a|5):\/\//.test(url)) { + return { + valid: false, + message: _( + "Invalid SOCKS URL: must start with socks4://, socks4a://, or socks5://" + ) + }; + } + if (!url || /\s/.test(url)) { + return { + valid: false, + message: _("Invalid SOCKS URL: must not contain spaces") + }; + } + const body = url.replace(/^socks(4|4a|5):\/\//, ""); + const [authAndHost] = body.split("#"); + const [credentials, hostPortPart] = authAndHost.includes("@") ? authAndHost.split("@") : [null, authAndHost]; + if (credentials) { + const [username, _password] = credentials.split(":"); + if (!username) { + return { + valid: false, + message: _("Invalid SOCKS URL: missing username") + }; + } + } + if (!hostPortPart) { + return { + valid: false, + message: _("Invalid SOCKS URL: missing host and port") + }; + } + const [host, port] = hostPortPart.split(":"); + if (!host) { + return { + valid: false, + message: _("Invalid SOCKS URL: missing hostname or IP") + }; + } + if (!port) { + return { valid: false, message: _("Invalid SOCKS URL: missing port") }; + } + const portNum = Number(port); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { + return { + valid: false, + message: _("Invalid SOCKS URL: invalid port number") + }; + } + const ipv4Result = validateIPV4(host); + const domainResult = validateDomain(host); + if (!ipv4Result.valid && !domainResult.valid) { + return { + valid: false, + message: _("Invalid SOCKS URL: invalid host format") + }; + } + } catch (_e) { + return { valid: false, message: _("Invalid SOCKS URL: parsing failed") }; + } + return { valid: true, message: _("Valid") }; +} + // src/validators/validateProxyUrl.ts function validateProxyUrl(url) { if (url.startsWith("ss://")) { @@ -394,9 +460,14 @@ function validateProxyUrl(url) { if (url.startsWith("trojan://")) { return validateTrojanUrl(url); } + if (/^socks(4|4a|5):\/\//.test(url)) { + return validateSocksUrl(url); + } return { valid: false, - message: _("URL must start with vless:// or ss:// or trojan://") + message: _( + "URL must start with vless://, ss://, trojan://, or socks4/5://" + ) }; } @@ -4490,6 +4561,7 @@ return baseclass.extend({ validatePath, validateProxyUrl, validateShadowsocksUrl, + validateSocksUrl, validateSubnet, validateTrojanUrl, validateUrl, diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js index 503a96d..0443d0d 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js @@ -43,7 +43,7 @@ function createSectionContent(section) { o.textarea = true; o.rmempty = false; 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'; + 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 \n// socks5://127.0.0.1:1080'; o.validate = function (section_id, value) { // Optional if (!value || value.length === 0) { @@ -102,7 +102,7 @@ function createSectionContent(section) { _('URLTest Proxy Links'), ); o.depends('proxy_config_type', 'urltest'); - o.placeholder = 'vless://, ss://, trojan:// links'; + o.placeholder = 'vless://, ss://, trojan://, socks4/5:// links'; o.rmempty = false; o.validate = function (section_id, value) { // Optional