mirror of
https://github.com/itdoginfo/podkop.git
synced 2026-01-02 22:58:58 +03:00
feat: add hy2 validator
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateHysteria2Url } from '../validateHysteriaUrl.js';
|
||||
|
||||
const validUrls = [
|
||||
// Basic password-only
|
||||
['password basic', 'hysteria2://pass@example.com:443/#hy2-basic'],
|
||||
|
||||
// insecure=1
|
||||
[
|
||||
'insecure allowed',
|
||||
'hysteria2://pass@example.com:443/?insecure=1#hy2-insecure',
|
||||
],
|
||||
|
||||
// SNI
|
||||
['SNI param', 'hysteria2://pass@example.com:443/?sni=google.com#hy2-sni'],
|
||||
|
||||
// Obfuscation
|
||||
[
|
||||
'Obfs + password',
|
||||
'hysteria2://mypassword@1.1.1.1:8443/?obfs=salamander&obfs-password=abc123#hy2-obfs',
|
||||
],
|
||||
|
||||
// All params
|
||||
[
|
||||
'All options combined',
|
||||
'hysteria2://pw@8.8.8.8:8443/?sni=example.com&obfs=salamander&obfs-password=hello&insecure=1#hy2-full',
|
||||
],
|
||||
|
||||
// Explicit obfs=none (valid)
|
||||
['obfs none = ok', 'hysteria2://pw@example.com:443/?obfs=none#hy2-none'],
|
||||
];
|
||||
|
||||
const invalidUrls = [
|
||||
['No prefix', 'pw@example.com:443'],
|
||||
['Missing password', 'hysteria2://@example.com:443/'],
|
||||
['Missing host', 'hysteria2://pw@:443/'],
|
||||
['Missing port', 'hysteria2://pw@example.com/'],
|
||||
['Non-numeric port', 'hysteria2://pw@example.com:port/'],
|
||||
['Port out of range', 'hysteria2://pw@example.com:99999/'],
|
||||
|
||||
// Obfuscation errors
|
||||
['Unknown obfs type', 'hysteria2://pw@example.com:443/?obfs=weird'],
|
||||
[
|
||||
'obfs without obfs-password',
|
||||
'hysteria2://pw@example.com:443/?obfs=salamander',
|
||||
],
|
||||
|
||||
// insecure only accepts 0/1
|
||||
['invalid insecure', 'hysteria2://pw@example.com:443/?insecure=5'],
|
||||
|
||||
// SNI empty
|
||||
['empty sni', 'hysteria2://pw@example.com:443/?sni='],
|
||||
];
|
||||
|
||||
describe('validateHysteria2Url', () => {
|
||||
describe.each(validUrls)('Valid HY2 URL: %s', (_desc, url) => {
|
||||
it(`returns valid=true for "${url}"`, () => {
|
||||
const res = validateHysteria2Url(url);
|
||||
expect(res.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(invalidUrls)('Invalid HY2 URL: %s', (_desc, url) => {
|
||||
it(`returns valid=false for "${url}"`, () => {
|
||||
const res = validateHysteria2Url(url);
|
||||
expect(res.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('detects invalid port range', () => {
|
||||
const res = validateHysteria2Url('hysteria2://pw@example.com:70000/');
|
||||
expect(res.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
115
fe-app-podkop/src/validators/validateHysteriaUrl.ts
Normal file
115
fe-app-podkop/src/validators/validateHysteriaUrl.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { ValidationResult } from './types';
|
||||
import { parseQueryString } from '../helpers/parseQueryString';
|
||||
|
||||
export function validateHysteria2Url(url: string): ValidationResult {
|
||||
try {
|
||||
const isHY2 = url.startsWith('hysteria2://');
|
||||
const isHY2Short = url.startsWith('hy2://');
|
||||
|
||||
if (!isHY2 && !isHY2Short) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: must start with hysteria2:// or hy2://'),
|
||||
};
|
||||
}
|
||||
|
||||
if (/\s/.test(url)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: must not contain spaces'),
|
||||
};
|
||||
}
|
||||
|
||||
const prefix = isHY2 ? 'hysteria2://' : 'hy2://';
|
||||
const body = url.slice(prefix.length);
|
||||
|
||||
const [mainPart] = body.split('#');
|
||||
const [authHostPort, queryString] = mainPart.split('?');
|
||||
|
||||
if (!authHostPort)
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: missing credentials/server'),
|
||||
};
|
||||
|
||||
const [passwordPart, hostPortPart] = authHostPort.split('@');
|
||||
|
||||
if (!passwordPart)
|
||||
return { valid: false, message: _('Invalid HY2 URL: missing password') };
|
||||
|
||||
if (!hostPortPart)
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: missing host & port'),
|
||||
};
|
||||
|
||||
const [host, port] = hostPortPart.split(':');
|
||||
|
||||
if (!host) {
|
||||
return { valid: false, message: _('Invalid HY2 URL: missing host') };
|
||||
}
|
||||
|
||||
if (!port) {
|
||||
return { valid: false, message: _('Invalid HY2 URL: missing port') };
|
||||
}
|
||||
|
||||
const cleanedPort = port.replace('/', '');
|
||||
const portNum = Number(cleanedPort);
|
||||
|
||||
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: invalid port number'),
|
||||
};
|
||||
}
|
||||
|
||||
if (queryString) {
|
||||
const params = parseQueryString(queryString);
|
||||
const paramsKeys = Object.keys(params);
|
||||
|
||||
if (
|
||||
paramsKeys.includes('insecure') &&
|
||||
!['0', '1'].includes(params.insecure)
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: insecure must be 0 or 1'),
|
||||
};
|
||||
}
|
||||
|
||||
const validObfsTypes = ['none', 'salamander'];
|
||||
|
||||
if (
|
||||
paramsKeys.includes('obfs') &&
|
||||
!validObfsTypes.includes(params.obfs)
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
message: _('Invalid HY2 URL: unsupported obfs type'),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
paramsKeys.includes('obfs') &&
|
||||
params.obfs !== 'none' &&
|
||||
!params['obfs-password']
|
||||
) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Invalid HY2 URL: obfs-password required when obfs is set',
|
||||
};
|
||||
}
|
||||
|
||||
if (paramsKeys.includes('sni') && !params.sni) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Invalid HY2 URL: sni cannot be empty',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true, message: _('Valid') };
|
||||
} catch (_e) {
|
||||
return { valid: false, message: _('Invalid HY2 URL: parsing failed') };
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { validateShadowsocksUrl } from './validateShadowsocksUrl';
|
||||
import { validateVlessUrl } from './validateVlessUrl';
|
||||
import { validateTrojanUrl } from './validateTrojanUrl';
|
||||
import { validateSocksUrl } from './validateSocksUrl';
|
||||
import { validateHysteria2Url } from './validateHysteriaUrl';
|
||||
|
||||
// TODO refactor current validation and add tests
|
||||
export function validateProxyUrl(url: string): ValidationResult {
|
||||
@@ -24,6 +25,13 @@ export function validateProxyUrl(url: string): ValidationResult {
|
||||
return validateSocksUrl(trimmedUrl);
|
||||
}
|
||||
|
||||
if (
|
||||
trimmedUrl.startsWith('hysteria2://') ||
|
||||
trimmedUrl.startsWith('hy2://')
|
||||
) {
|
||||
return validateHysteria2Url(trimmedUrl);
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
message: _(
|
||||
|
||||
Reference in New Issue
Block a user