From 9501a60dc027142ec97152992c646aa5863a0b5d Mon Sep 17 00:00:00 2001 From: sharevb Date: Sat, 20 Apr 2024 22:19:59 +0200 Subject: [PATCH 1/2] feat(new tool): IPv4/6 in IP Range --- package.json | 1 + pnpm-lock.yaml | 19 ++++-- src/tools/index.ts | 11 +++- src/tools/ip-in-range/index.ts | 12 ++++ src/tools/ip-in-range/ip-in-range.vue | 91 +++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 src/tools/ip-in-range/index.ts create mode 100644 src/tools/ip-in-range/ip-in-range.vue diff --git a/package.json b/package.json index ffa7bd4e..3df2a488 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "highlight.js": "^11.7.0", "iarna-toml-esm": "^3.0.5", "ibantools": "^4.3.3", + "ip-matching": "^2.1.2", "json5": "^2.2.3", "jwt-decode": "^3.1.2", "libphonenumber-js": "^1.10.28", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dfacabd4..a1a70c26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ dependencies: ibantools: specifier: ^4.3.3 version: 4.3.3 + ip-matching: + specifier: ^2.1.2 + version: 2.1.2 json5: specifier: ^2.2.3 version: 2.2.3 @@ -3374,7 +3377,7 @@ packages: dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 10.6.1(vue@3.3.4) + '@vueuse/shared': 10.9.0(vue@3.3.4) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: @@ -4016,10 +4019,10 @@ packages: - vue dev: false - /@vueuse/shared@10.6.1(vue@3.3.4): - resolution: {integrity: sha512-TECVDTIedFlL0NUfHWncf3zF9Gc4VfdxfQc8JFwoVZQmxpONhLxFrlm0eHQeidHj4rdTPL3KXJa0TZCk1wnc5Q==} + /@vueuse/shared@10.9.0(vue@3.3.4): + resolution: {integrity: sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==} dependencies: - vue-demi: 0.14.6(vue@3.3.4) + vue-demi: 0.14.7(vue@3.3.4) transitivePeerDependencies: - '@vue/composition-api' - vue @@ -6203,6 +6206,10 @@ packages: jsbn: 1.1.0 dev: false + /ip-matching@2.1.2: + resolution: {integrity: sha512-/ok+VhKMasgR5gvTRViwRFQfc0qYt9Vdowg6TO4/pFlDCob5ZjGPkwuOoQVCd5OrMm20zqh+1vA8KLJZTeWudg==} + dev: false + /is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} dev: true @@ -9200,8 +9207,8 @@ packages: vue: 3.3.4 dev: false - /vue-demi@0.14.6(vue@3.3.4): - resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} + /vue-demi@0.14.7(vue@3.3.4): + resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} engines: {node: '>=12'} hasBin: true requiresBuild: true diff --git a/src/tools/index.ts b/src/tools/index.ts index 52bdf8e3..efb1f8f1 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -75,6 +75,7 @@ import { tool as urlParser } from './url-parser'; import { tool as uuidGenerator } from './uuid-generator'; import { tool as macAddressLookup } from './mac-address-lookup'; import { tool as xmlFormatter } from './xml-formatter'; +import { tool as ipInRange } from './ip-in-range'; export const toolsByCategory: ToolCategory[] = [ { @@ -143,7 +144,15 @@ export const toolsByCategory: ToolCategory[] = [ }, { name: 'Network', - components: [ipv4SubnetCalculator, ipv4AddressConverter, ipv4RangeExpander, macAddressLookup, macAddressGenerator, ipv6UlaGenerator], + components: [ + ipv4SubnetCalculator, + ipv4AddressConverter, + ipInRange, + ipv4RangeExpander, + macAddressLookup, + macAddressGenerator, + ipv6UlaGenerator, + ], }, { name: 'Math', diff --git a/src/tools/ip-in-range/index.ts b/src/tools/ip-in-range/index.ts new file mode 100644 index 00000000..b8154b45 --- /dev/null +++ b/src/tools/ip-in-range/index.ts @@ -0,0 +1,12 @@ +import { UnfoldMoreOutlined } from '@vicons/material'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'IP in Range/CIDR/Mask', + path: '/ip-in-range', + description: 'Given a CIDR/IP Range/Wildcard IP/IP Mask, tell if a given IP is in subnet range', + keywords: ['ip', 'cidr', 'range'], + component: () => import('./ip-in-range.vue'), + icon: UnfoldMoreOutlined, + createdAt: new Date('2024-03-09'), +}); diff --git a/src/tools/ip-in-range/ip-in-range.vue b/src/tools/ip-in-range/ip-in-range.vue new file mode 100644 index 00000000..4e023b35 --- /dev/null +++ b/src/tools/ip-in-range/ip-in-range.vue @@ -0,0 +1,91 @@ + + + From bb6995320c15239bea2e818b8d9f2fb6e03f22f4 Mon Sep 17 00:00:00 2001 From: ShareVB Date: Sun, 12 Jan 2025 14:22:13 +0100 Subject: [PATCH 2/2] feat(CIDR in CIDR): IP Range (single IP, CIDR, range, mask) in IP Range (CIDR, range, mask) --- components.d.ts | 12 ++--- .../cidr-in-cidr/cidr-in-cidr.service.test.ts | 48 ++++++++++++++++++ .../cidr-in-cidr/cidr-in-cidr.service.ts | 28 +++++++++++ .../cidr-in-cidr.vue} | 49 +++++++------------ src/tools/cidr-in-cidr/index.ts | 13 +++++ src/tools/index.ts | 4 +- src/tools/ip-in-range/index.ts | 12 ----- 7 files changed, 114 insertions(+), 52 deletions(-) create mode 100644 src/tools/cidr-in-cidr/cidr-in-cidr.service.test.ts create mode 100644 src/tools/cidr-in-cidr/cidr-in-cidr.service.ts rename src/tools/{ip-in-range/ip-in-range.vue => cidr-in-cidr/cidr-in-cidr.vue} (53%) create mode 100644 src/tools/cidr-in-cidr/index.ts delete mode 100644 src/tools/ip-in-range/index.ts diff --git a/components.d.ts b/components.d.ts index e31119b3..c5a10bee 100644 --- a/components.d.ts +++ b/components.d.ts @@ -37,6 +37,7 @@ declare module '@vue/runtime-core' { 'CFileUpload.demo': typeof import('./src/ui/c-file-upload/c-file-upload.demo.vue')['default'] ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default'] Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] + CidrInCidr: typeof import('./src/tools/cidr-in-cidr/cidr-in-cidr.vue')['default'] CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default'] 'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default'] @@ -102,6 +103,7 @@ declare module '@vue/runtime-core' { IconMdiTriangleDown: typeof import('~icons/mdi/triangle-down')['default'] InputCopyable: typeof import('./src/components/InputCopyable.vue')['default'] IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default'] + IpInRange: typeof import('./src/tools/cidr-in-cidr/cidr-in-cidr.vue')['default'] Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default'] Ipv4RangeExpander: typeof import('./src/tools/ipv4-range-expander/ipv4-range-expander.vue')['default'] Ipv4SubnetCalculator: typeof import('./src/tools/ipv4-subnet-calculator/ipv4-subnet-calculator.vue')['default'] @@ -127,24 +129,17 @@ declare module '@vue/runtime-core' { MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] - NCode: typeof import('naive-ui')['NCode'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NDivider: typeof import('naive-ui')['NDivider'] NEllipsis: typeof import('naive-ui')['NEllipsis'] - NFormItem: typeof import('naive-ui')['NFormItem'] - NGi: typeof import('naive-ui')['NGi'] - NGrid: typeof import('naive-ui')['NGrid'] NH1: typeof import('naive-ui')['NH1'] NH3: typeof import('naive-ui')['NH3'] NIcon: typeof import('naive-ui')['NIcon'] - NInputNumber: typeof import('naive-ui')['NInputNumber'] - NLabel: typeof import('naive-ui')['NLabel'] NLayout: typeof import('naive-ui')['NLayout'] NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] - NScrollbar: typeof import('naive-ui')['NScrollbar'] - NSpin: typeof import('naive-ui')['NSpin'] + NTable: typeof import('naive-ui')['NTable'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] @@ -159,6 +154,7 @@ declare module '@vue/runtime-core' { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default'] + SafelinkDecoder: typeof import('./src/tools/safelink-decoder/safelink-decoder.vue')['default'] SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default'] SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default'] diff --git a/src/tools/cidr-in-cidr/cidr-in-cidr.service.test.ts b/src/tools/cidr-in-cidr/cidr-in-cidr.service.test.ts new file mode 100644 index 00000000..8b74d9ac --- /dev/null +++ b/src/tools/cidr-in-cidr/cidr-in-cidr.service.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; +import { cidrInCidr } from './cidr-in-cidr.service'; + +describe('cidr-in-cidr', () => { + describe('cidrInCidr', () => { + it('should return correct inclusion', () => { + expect(cidrInCidr({ baseRange: '192.168.0.0/16', ipOrRangeToTest: '192.168.0.1' }).isIncluded).toBe(true); // NOSONAR + expect(cidrInCidr({ baseRange: '192.168.1.0/24', ipOrRangeToTest: '192.168.0.10' }).isIncluded).toBe(false); // NOSONAR + expect(cidrInCidr({ baseRange: '192.168.1.*', ipOrRangeToTest: '192.168.0.10' }).isIncluded).toBe(false); // NOSONAR + + expect(cidrInCidr({ baseRange: '192.168.0.0/24', ipOrRangeToTest: '192.168.0.0/28' }).isIncluded).toBe(true); // NOSONAR + expect(cidrInCidr({ baseRange: '192.168.20.0/24', ipOrRangeToTest: '192.168.1.0/28' }).isIncluded).toBe(false); // NOSONAR + expect(cidrInCidr({ baseRange: '192.168.20.*', ipOrRangeToTest: '192.168.1.0/28' }).isIncluded).toBe(false); // NOSONAR + expect(cidrInCidr({ baseRange: '192.168.0.*', ipOrRangeToTest: '192.168.0.0/28' }).isIncluded).toBe(true); // NOSONAR + + expect(cidrInCidr({ baseRange: '192.168.20.1-192.168.20.15', ipOrRangeToTest: '192.168.20.12' }).isIncluded).toBe(true); // NOSONAR + expect(cidrInCidr({ baseRange: '192.168.20.1-192.168.20.15', ipOrRangeToTest: '192.168.20.20' }).isIncluded).toBe(false); // NOSONAR + + expect(cidrInCidr({ baseRange: '192.168.20.1-192.168.20.15', ipOrRangeToTest: '192.168.20.9-192.168.20.12' }).isIncluded).toBe(true); // NOSONAR + expect(cidrInCidr({ baseRange: '192.168.20.1-192.168.20.15', ipOrRangeToTest: '192.168.20.9-192.168.20.20' }).isIncluded).toBe(false); // NOSONAR + + expect(cidrInCidr({ baseRange: '192.168.20.0-192.168.20.255', ipOrRangeToTest: '192.168.20.0/28' }).isIncluded).toBe(true); // NOSONAR + expect(cidrInCidr({ baseRange: '192.168.20.0-192.168.20.255', ipOrRangeToTest: '192.168.1.0/28' }).isIncluded).toBe(false); // NOSONAR + expect(cidrInCidr({ baseRange: '192.168.0.0-192.168.1.255', ipOrRangeToTest: '192.168.1.0/28' }).isIncluded).toBe(true); // NOSONAR + }); + it('should return correct subnet', () => { + expect(cidrInCidr({ baseRange: '192.168.0.0/16', ipOrRangeToTest: '192.168.0.1' }).baseSubnets).to.deep.eq([ // NOSONAR + { + cidr: '192.168.0.0/16', // NOSONAR + end: '192.168.255.255', // NOSONAR + start: '192.168.0.0', // NOSONAR + }, + ]); + expect(cidrInCidr({ baseRange: '192.168.20.1-192.168.20.3', ipOrRangeToTest: '192.168.1.0/28' }).baseSubnets).to.deep.eq([ // NOSONAR + { + cidr: '192.168.20.1/32', // NOSONAR + end: '192.168.20.1', // NOSONAR + start: '192.168.20.1', // NOSONAR + }, + { + cidr: '192.168.20.2/31', // NOSONAR + end: '192.168.20.3', // NOSONAR + start: '192.168.20.2', // NOSONAR + }, + ]); + }); + }); +}); diff --git a/src/tools/cidr-in-cidr/cidr-in-cidr.service.ts b/src/tools/cidr-in-cidr/cidr-in-cidr.service.ts new file mode 100644 index 00000000..3850b329 --- /dev/null +++ b/src/tools/cidr-in-cidr/cidr-in-cidr.service.ts @@ -0,0 +1,28 @@ +import { getMatch } from 'ip-matching'; + +export function cidrInCidr( + { baseRange, ipOrRangeToTest }: + { + baseRange: string + ipOrRangeToTest: string + }) { + const baseMatchMasks = getMatch(baseRange)?.convertToMasks() || []; + const ipOrRangeToTestMatchMasks = getMatch(ipOrRangeToTest)?.convertToMasks() || []; + + const baseSubnets = baseMatchMasks.map((mask) => { + const subnet = mask.convertToSubnet(); + if (!subnet) { + return { cidr: '', start: '', end: '' }; + } + return { + cidr: subnet.toString(), + start: subnet.getFirst().toString(), + end: subnet.getLast().toString(), + }; + }).filter(subnet => subnet.cidr !== ''); + + return { + baseSubnets, + isIncluded: baseMatchMasks.some(baseMask => ipOrRangeToTestMatchMasks.every(ipOrRangeMask => ipOrRangeMask.isSubsetOf(baseMask))), + }; +} diff --git a/src/tools/ip-in-range/ip-in-range.vue b/src/tools/cidr-in-cidr/cidr-in-cidr.vue similarity index 53% rename from src/tools/ip-in-range/ip-in-range.vue rename to src/tools/cidr-in-cidr/cidr-in-cidr.vue index 4e023b35..84ab3614 100644 --- a/src/tools/ip-in-range/ip-in-range.vue +++ b/src/tools/cidr-in-cidr/cidr-in-cidr.vue @@ -2,33 +2,22 @@ import { useStorage } from '@vueuse/core'; import { Check as CheckIcon, LetterX as CrossIcon } from '@vicons/tabler'; import { getMatch } from 'ip-matching'; +import { cidrInCidr } from './cidr-in-cidr.service'; import { withDefaultOnError } from '@/utils/defaults'; import { isNotThrowing } from '@/utils/boolean'; import SpanCopyable from '@/components/SpanCopyable.vue'; -const range = useStorage('ip-in-range:range', '192.168.0.1/24'); // NOSONAR -const ip = useStorage('ip-in-range:ip', '192.168.0.1'); // NOSONAR +const baseRange = useStorage('cidr-in-cidr:range', '192.168.0.1/24'); // NOSONAR +const ipOrRangeToTest = useStorage('cidr-in-cidr:ip', '192.168.0.1'); // NOSONAR -const match = computed(() => withDefaultOnError(() => getMatch(range.value), undefined)); -const subnets = computed(() => { - return (match.value?.convertToMasks() || []).map((mask) => { - const subnet = mask.convertToSubnet(); - if (!subnet) { - return { cidr: '', start: '', end: '' }; - } - return { - cidr: subnet.toString(), - start: subnet.getFirst().toString(), - end: subnet.getLast().toString(), - }; - }).filter(subnet => subnet.cidr !== ''); -}); -const ipIsInMatch = computed(() => match.value?.matches(ip.value)); +const matchResult = computed(() => withDefaultOnError( + () => cidrInCidr({ baseRange: baseRange.value, ipOrRangeToTest: ipOrRangeToTest.value }), + { baseSubnets: [], isIncluded: false })); -const ipValidationRules = [ +const rangeValidationRules = [ { message: 'We cannot parse this CIDR/IP Range/Mask/Wildcard, check the format', - validator: (value: string) => isNotThrowing(() => getMatch(value)) && getMatch(range.value), + validator: (value: string) => isNotThrowing(() => getMatch(value)) && getMatch(value), }, ]; @@ -36,35 +25,35 @@ const ipValidationRules = [