From e6731efa4341d27103cba20399c00d2d849dc138 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 9 Feb 2024 09:51:13 -0800 Subject: [PATCH 1/3] Move IP validation to util file; add test --- app/forms/ip-pool-range-add.tsx | 8 +------- libs/util/str.spec.ts | 25 +++++++++++++++++++++++++ libs/util/str.ts | 10 ++++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/app/forms/ip-pool-range-add.tsx b/app/forms/ip-pool-range-add.tsx index f3ec199c2e..a32671fbfd 100644 --- a/app/forms/ip-pool-range-add.tsx +++ b/app/forms/ip-pool-range-add.tsx @@ -9,7 +9,7 @@ import { useNavigate } from 'react-router-dom' import { useApiMutation, useApiQueryClient, type IpRange } from '@oxide/api' import { Message } from '@oxide/ui' -import { IPV4_REGEX, IPV6_REGEX } from '@oxide/util' +import { validateIp } from '@oxide/util' import { SideModalForm, TextField } from 'app/components/form' import { useForm, useIpPoolSelector } from 'app/hooks' @@ -21,12 +21,6 @@ const defaultValues: IpRange = { last: '', } -function validateIp(s: string) { - const isv4 = IPV4_REGEX.test(s) - const isv6 = !isv4 && IPV6_REGEX.test(s) - return { isv4, isv6, valid: isv4 || isv6 } -} - const invalidAddressError = { type: 'pattern', message: 'Not a valid IP address' } const diffVersionError = { diff --git a/libs/util/str.spec.ts b/libs/util/str.spec.ts index 629d2be1fd..e83e26f425 100644 --- a/libs/util/str.spec.ts +++ b/libs/util/str.spec.ts @@ -15,6 +15,7 @@ import { IPV6_REGEX, kebabCase, titleCase, + validateIp, } from './str' describe('capitalize', () => { @@ -90,6 +91,7 @@ describe('titleCase', () => { test.each(['123.4.56.7', '1.2.3.4'])('ipv4Regex passes: %s', (s) => { expect(IPV4_REGEX.test(s)).toBe(true) + expect(validateIp(s)).toStrictEqual({ isv4: true, isv6: false, valid: true }) }) test.each([ @@ -131,6 +133,7 @@ test.each([ 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', ])('ipv6Regex passes: %s', (s) => { expect(IPV6_REGEX.test(s)).toBe(true) + expect(validateIp(s)).toStrictEqual({ isv4: false, isv6: true, valid: true }) }) test.each([ @@ -154,3 +157,25 @@ test.each([ ])('ipv6Regex fails: %s', (s) => { expect(IPV6_REGEX.test(s)).toBe(false) }) + +test.each([ + '', + '1', + 'abc', + '256.1.1.1', + '2001:0db8:85a3:0000:0000:8a2e:0370:7334 ', + ' 2001:db8::', + '1:2:3:4:5:6:7:8:9', + '1:2:3:4:5:6::7:8', + ':1:2:3:4:5:6:7:8', + '1:2:3:4:5:6:7:8:', + '::1:2:3:4:5:6:7:8', + '1:2:3:4:5:6:7:8::', + '1:2:3:4:5:6:7:88888', + '2001:db8:3:4:5::192.0.2.33', // std::new::Ipv6Net allows this one + 'fe08::7:8%', + 'fe08::7:8i', + 'fe08::7:8interface', +])('validateIp catches invalid IP: %s', (s) => { + expect(validateIp(s)).toStrictEqual({ isv4: false, isv6: false, valid: false }) +}) diff --git a/libs/util/str.ts b/libs/util/str.ts index 4c6fd9d95a..1585c76cdf 100644 --- a/libs/util/str.ts +++ b/libs/util/str.ts @@ -57,5 +57,15 @@ export const titleCase = (text: string): string => { export const IPV4_REGEX = /^(?:(?:[1-9]|1\d|2[0-4])?\d|25[0-5])(?:\.(?:(?:[1-9]|1\d|2[0-4])?\d|25[0-5])){3}$/u +export const isValidIpV4 = (ip: string) => IPV4_REGEX.test(ip) + export const IPV6_REGEX = /^(?:(?:[\da-f]{1,4}:){7}[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,7}:|(?:[\da-f]{1,4}:){1,6}:[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,5}(?::[\da-f]{1,4}){1,2}|(?:[\da-f]{1,4}:){1,4}(?::[\da-f]{1,4}){1,3}|(?:[\da-f]{1,4}:){1,3}(?::[\da-f]{1,4}){1,4}|(?:[\da-f]{1,4}:){1,2}(?::[\da-f]{1,4}){1,5}|[\da-f]{1,4}:(?::[\da-f]{1,4}){1,6}|:(?:(?::[\da-f]{1,4}){1,7}|:)|fe80:(?::[\da-f]{0,4}){0,4}%[\da-z]+|::(?:f{4}(?::0{1,4})?:)?(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d)|(?:[\da-f]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d))$/iu + +export const isValidIpV6 = (ip: string) => IPV6_REGEX.test(ip) + +export const validateIp = (ip: string) => { + const isv4 = isValidIpV4(ip) + const isv6 = !isv4 && isValidIpV6(ip) + return { isv4, isv6, valid: isv4 || isv6 } +} From b64b614dd3cadfcede4aecbbedb64137681929b3 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 9 Feb 2024 09:55:19 -0800 Subject: [PATCH 2/3] Test was simpler than expected, so no need to set up extra functions --- libs/util/str.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/libs/util/str.ts b/libs/util/str.ts index 1585c76cdf..191526ba64 100644 --- a/libs/util/str.ts +++ b/libs/util/str.ts @@ -57,15 +57,11 @@ export const titleCase = (text: string): string => { export const IPV4_REGEX = /^(?:(?:[1-9]|1\d|2[0-4])?\d|25[0-5])(?:\.(?:(?:[1-9]|1\d|2[0-4])?\d|25[0-5])){3}$/u -export const isValidIpV4 = (ip: string) => IPV4_REGEX.test(ip) - export const IPV6_REGEX = /^(?:(?:[\da-f]{1,4}:){7}[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,7}:|(?:[\da-f]{1,4}:){1,6}:[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,5}(?::[\da-f]{1,4}){1,2}|(?:[\da-f]{1,4}:){1,4}(?::[\da-f]{1,4}){1,3}|(?:[\da-f]{1,4}:){1,3}(?::[\da-f]{1,4}){1,4}|(?:[\da-f]{1,4}:){1,2}(?::[\da-f]{1,4}){1,5}|[\da-f]{1,4}:(?::[\da-f]{1,4}){1,6}|:(?:(?::[\da-f]{1,4}){1,7}|:)|fe80:(?::[\da-f]{0,4}){0,4}%[\da-z]+|::(?:f{4}(?::0{1,4})?:)?(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d)|(?:[\da-f]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d))$/iu -export const isValidIpV6 = (ip: string) => IPV6_REGEX.test(ip) - export const validateIp = (ip: string) => { - const isv4 = isValidIpV4(ip) - const isv6 = !isv4 && isValidIpV6(ip) + const isv4 = IPV4_REGEX.test(ip) + const isv6 = !isv4 && IPV6_REGEX.test(ip) return { isv4, isv6, valid: isv4 || isv6 } } From 99883c5f103ab2fdf62b4958c948faccca5354a6 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 9 Feb 2024 12:15:09 -0800 Subject: [PATCH 3/3] Refactor tests --- libs/util/str.spec.ts | 77 +++++++++++-------------------------------- libs/util/str.ts | 4 +-- 2 files changed, 21 insertions(+), 60 deletions(-) diff --git a/libs/util/str.spec.ts b/libs/util/str.spec.ts index e83e26f425..37115bd02e 100644 --- a/libs/util/str.spec.ts +++ b/libs/util/str.spec.ts @@ -7,16 +7,7 @@ */ import { describe, expect, it, test } from 'vitest' -import { - camelCase, - capitalize, - commaSeries, - IPV4_REGEX, - IPV6_REGEX, - kebabCase, - titleCase, - validateIp, -} from './str' +import { camelCase, capitalize, commaSeries, kebabCase, titleCase, validateIp } from './str' describe('capitalize', () => { it('capitalizes the first letter', () => { @@ -89,30 +80,12 @@ describe('titleCase', () => { // Rust playground comparing results with std::net::{Ipv4Addr, Ipv6Addr} // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=493b3345b9f6c0b1c8ee91834e99ef7b -test.each(['123.4.56.7', '1.2.3.4'])('ipv4Regex passes: %s', (s) => { - expect(IPV4_REGEX.test(s)).toBe(true) - expect(validateIp(s)).toStrictEqual({ isv4: true, isv6: false, valid: true }) -}) - -test.each([ - '', - '1', - 'abc', - 'a.b.c.d', - // some implementations (I think incorrectly) allow leading zeros but nexus does not - '01.102.103.104', - '::ffff:192.0.2.128', - '127.0.0', - '127.0.0.1.', - '127.0.0.1 ', - ' 127.0.0.1', - '10002.3.4', - '1.2.3.4.5', - '256.0.0.0', - '260.0.0.0', -])('ipv4Regex fails: %s', (s) => { - expect(IPV4_REGEX.test(s)).toBe(false) -}) +test.each(['123.4.56.7', '1.2.3.4'])( + 'validateIp catches valid IPV4 / invalid IPV6: %s', + (s) => { + expect(validateIp(s)).toStrictEqual({ isv4: true, isv6: false, valid: true }) + } +) test.each([ '2001:db8:3333:4444:5555:6666:7777:8888', @@ -131,8 +104,7 @@ test.each([ '::ffff:255.255.255.255', 'fe08::7:8', 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', -])('ipv6Regex passes: %s', (s) => { - expect(IPV6_REGEX.test(s)).toBe(true) +])('validateIp catches invalid IPV4 / valid IPV6: %s', (s) => { expect(validateIp(s)).toStrictEqual({ isv4: false, isv6: true, valid: true }) }) @@ -140,28 +112,17 @@ test.each([ '', '1', 'abc', - '123.4.56.7', - '2001:0db8:85a3:0000:0000:8a2e:0370:7334 ', - ' 2001:db8::', - '1:2:3:4:5:6:7:8:9', - '1:2:3:4:5:6::7:8', - ':1:2:3:4:5:6:7:8', - '1:2:3:4:5:6:7:8:', - '::1:2:3:4:5:6:7:8', - '1:2:3:4:5:6:7:8::', - '1:2:3:4:5:6:7:88888', - '2001:db8:3:4:5::192.0.2.33', // std::new::Ipv6Net allows this one - 'fe08::7:8%', - 'fe08::7:8i', - 'fe08::7:8interface', -])('ipv6Regex fails: %s', (s) => { - expect(IPV6_REGEX.test(s)).toBe(false) -}) - -test.each([ - '', - '1', - 'abc', + 'a.b.c.d', + // some implementations (I think incorrectly) allow leading zeros but nexus does not + '01.102.103.104', + '127.0.0', + '127.0.0.1.', + '127.0.0.1 ', + ' 127.0.0.1', + '10002.3.4', + '1.2.3.4.5', + '256.0.0.0', + '260.0.0.0', '256.1.1.1', '2001:0db8:85a3:0000:0000:8a2e:0370:7334 ', ' 2001:db8::', diff --git a/libs/util/str.ts b/libs/util/str.ts index 191526ba64..55cde1f7bb 100644 --- a/libs/util/str.ts +++ b/libs/util/str.ts @@ -54,10 +54,10 @@ export const titleCase = (text: string): string => { // but they didn't match results with std::new on simple test cases // https://github.com/fabian-hiller/valibot/blob/2554aea5/library/src/regex.ts#L43-L54 -export const IPV4_REGEX = +const IPV4_REGEX = /^(?:(?:[1-9]|1\d|2[0-4])?\d|25[0-5])(?:\.(?:(?:[1-9]|1\d|2[0-4])?\d|25[0-5])){3}$/u -export const IPV6_REGEX = +const IPV6_REGEX = /^(?:(?:[\da-f]{1,4}:){7}[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,7}:|(?:[\da-f]{1,4}:){1,6}:[\da-f]{1,4}|(?:[\da-f]{1,4}:){1,5}(?::[\da-f]{1,4}){1,2}|(?:[\da-f]{1,4}:){1,4}(?::[\da-f]{1,4}){1,3}|(?:[\da-f]{1,4}:){1,3}(?::[\da-f]{1,4}){1,4}|(?:[\da-f]{1,4}:){1,2}(?::[\da-f]{1,4}){1,5}|[\da-f]{1,4}:(?::[\da-f]{1,4}){1,6}|:(?:(?::[\da-f]{1,4}){1,7}|:)|fe80:(?::[\da-f]{0,4}){0,4}%[\da-z]+|::(?:f{4}(?::0{1,4})?:)?(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d)|(?:[\da-f]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1?\d)?\d)\.){3}(?:25[0-5]|(?:2[0-4]|1?\d)?\d))$/iu export const validateIp = (ip: string) => {