diff --git a/fe-app-podkop/src/validators/index.ts b/fe-app-podkop/src/validators/index.ts index 5d0851d..b7ee5af 100644 --- a/fe-app-podkop/src/validators/index.ts +++ b/fe-app-podkop/src/validators/index.ts @@ -3,3 +3,4 @@ export * from './validateDomain'; export * from './validateDns'; export * from './validateUrl'; export * from './validatePath'; +export * from './validateSubnet'; diff --git a/fe-app-podkop/src/validators/tests/validateSubnet.test.js b/fe-app-podkop/src/validators/tests/validateSubnet.test.js new file mode 100644 index 0000000..13621b7 --- /dev/null +++ b/fe-app-podkop/src/validators/tests/validateSubnet.test.js @@ -0,0 +1,41 @@ +import { describe, it, expect } from 'vitest'; +import { validateSubnet } from '../validateSubnet'; + +export const validSubnets = [ + ['Simple IP', '192.168.1.1'], + ['With CIDR /24', '192.168.1.1/24'], + ['CIDR /0', '10.0.0.1/0'], + ['CIDR /32', '172.16.0.1/32'], + ['Loopback', '127.0.0.1'], + ['Broadcast with mask', '255.255.255.255/32'], +]; + +export const invalidSubnets = [ + ['Empty string', ''], + ['Bad format letters', 'abc.def.ghi.jkl'], + ['Octet too large', '300.1.1.1'], + ['Negative octet', '-1.2.3.4'], + ['Too many octets', '1.2.3.4.5'], + ['Not enough octets', '192.168.1'], + ['Leading zero octet', '01.2.3.4'], + ['Invalid CIDR (too high)', '192.168.1.1/33'], + ['Invalid CIDR (negative)', '192.168.1.1/-1'], + ['CIDR not number', '192.168.1.1/abc'], + ['Forbidden 0.0.0.0', '0.0.0.0'], +]; + +describe('validateSubnet', () => { + describe.each(validSubnets)('Valid subnet: %s', (_desc, subnet) => { + it(`returns {valid:true} for "${subnet}"`, () => { + const res = validateSubnet(subnet); + expect(res.valid).toBe(true); + }); + }); + + describe.each(invalidSubnets)('Invalid subnet: %s', (_desc, subnet) => { + it(`returns {valid:false} for "${subnet}"`, () => { + const res = validateSubnet(subnet); + expect(res.valid).toBe(false); + }); + }); +}); diff --git a/fe-app-podkop/src/validators/validateSubnet.ts b/fe-app-podkop/src/validators/validateSubnet.ts new file mode 100644 index 0000000..f0022c0 --- /dev/null +++ b/fe-app-podkop/src/validators/validateSubnet.ts @@ -0,0 +1,39 @@ +import { ValidationResult } from './types.js'; +import { validateIPV4 } from './validateIp'; + +export function validateSubnet(value: string): ValidationResult { + // Must be in form X.X.X.X or X.X.X.X/Y + const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?$/; + + if (!subnetRegex.test(value)) { + return { + valid: false, + message: 'Invalid format. Use X.X.X.X or X.X.X.X/Y', + }; + } + + const [ip, cidr] = value.split('/'); + + if (ip === '0.0.0.0') { + return { valid: false, message: 'IP address 0.0.0.0 is not allowed' }; + } + + const ipCheck = validateIPV4(ip); + if (!ipCheck.valid) { + return ipCheck; + } + + // Validate CIDR if present + if (cidr) { + const cidrNum = parseInt(cidr, 10); + + if (cidrNum < 0 || cidrNum > 32) { + return { + valid: false, + message: 'CIDR must be between 0 and 32', + }; + } + } + + 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 b5a256b..12e7e65 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 @@ -508,23 +508,18 @@ function createConfigSection(section, map, network) { o.rmempty = false; o.ucisection = s.section; o.validate = function (section_id, value) { - if (!value || value.length === 0) return true; - const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/; - if (!subnetRegex.test(value)) return _('Invalid format. Use format: X.X.X.X or X.X.X.X/Y'); - const [ip, cidr] = value.split('/'); - if (ip === "0.0.0.0") { - return _('IP address 0.0.0.0 is not allowed'); + // Optional + if (!value || value.length === 0) { + return true } - const ipParts = ip.split('.'); - for (const part of ipParts) { - const num = parseInt(part); - if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255'); + + const validation = main.validateSubnet(value); + + if (validation.valid) { + return true; } - if (cidr !== undefined) { - const cidrNum = parseInt(cidr); - if (cidrNum < 0 || cidrNum > 32) return _('CIDR must be between 0 and 32'); - } - return true; + + return _(validation.message) }; 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 //')); 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 6e7f3e7..18912a3 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 @@ -80,6 +80,35 @@ function validatePath(value) { }; } +// src/validators/validateSubnet.ts +function validateSubnet(value) { + const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?$/; + if (!subnetRegex.test(value)) { + return { + valid: false, + message: "Invalid format. Use X.X.X.X or X.X.X.X/Y" + }; + } + const [ip, cidr] = value.split("/"); + if (ip === "0.0.0.0") { + return { valid: false, message: "IP address 0.0.0.0 is not allowed" }; + } + const ipCheck = validateIPV4(ip); + if (!ipCheck.valid) { + return ipCheck; + } + if (cidr) { + const cidrNum = parseInt(cidr, 10); + if (cidrNum < 0 || cidrNum > 32) { + return { + valid: false, + message: "CIDR must be between 0 and 32" + }; + } + } + return { valid: true, message: "Valid" }; +} + // src/constants.ts var STATUS_COLORS = { SUCCESS: "#4caf50", @@ -218,5 +247,6 @@ return baseclass.extend({ validateDomain, validateIPV4, validatePath, + validateSubnet, validateUrl });