feat: add socks support

This commit is contained in:
divocat
2025-10-20 21:24:08 +03:00
parent 49dd1d608f
commit f0290fcc9e
7 changed files with 221 additions and 5 deletions

View File

@@ -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');
}
},

View File

@@ -10,3 +10,4 @@ export * from './validateVlessUrl';
export * from './validateOutboundJson';
export * from './validateTrojanUrl';
export * from './validateProxyUrl';
export * from './validateSocksUrl';

View File

@@ -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);
});
});

View File

@@ -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://',
),
};
}

View File

@@ -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') };
}

View File

@@ -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,

View File

@@ -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