Skip to content

Commit

Permalink
allow ipv6 input
Browse files Browse the repository at this point in the history
  • Loading branch information
t-aleksander committed Nov 8, 2024
1 parent e997db0 commit 67b7522
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 33 deletions.
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"get-text-width": "^1.0.3",
"hex-rgb": "^5.0.0",
"html-react-parser": "^5.1.1",
"ipaddr.js": "^2.2.0",
"itertools": "^2.2.3",
"lodash-es": "^4.17.21",
"numbro": "^2.4.0",
Expand Down
9 changes: 9 additions & 0 deletions web/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web/src/i18n/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,7 @@ const en: BaseTranslation = {
portMax: 'Maximum port is 65535.',
endpoint: 'Enter a valid endpoint.',
address: 'Enter a valid address.',
addressNetmask: 'Enter a valid address with a netmask.',
validPort: 'Enter a valid port.',
validCode: 'Code should have 6 digits.',
allowedIps: 'Only valid IP or domain is allowed.',
Expand Down
8 changes: 8 additions & 0 deletions web/src/i18n/i18n-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2101,6 +2101,10 @@ type RootTranslation = {
* E​n​t​e​r​ ​a​ ​v​a​l​i​d​ ​a​d​d​r​e​s​s​.
*/
address: string
/**
* E​n​t​e​r​ ​a​ ​v​a​l​i​d​ ​a​d​d​r​e​s​s​ ​w​i​t​h​ ​a​ ​n​e​t​m​a​s​k​.
*/
addressNetmask: string
/**
* E​n​t​e​r​ ​a​ ​v​a​l​i​d​ ​p​o​r​t​.
*/
Expand Down Expand Up @@ -6356,6 +6360,10 @@ export type TranslationFunctions = {
* Enter a valid address.
*/
address: () => LocalizedString
/**
* Enter a valid address with a netmask.
*/
addressNetmask: () => LocalizedString
/**
* Enter a valid port.
*/
Expand Down
3 changes: 2 additions & 1 deletion web/src/i18n/pl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -841,8 +841,9 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe
oneUppercase: 'Wymagana jedna duża litera.',
oneLowercase: 'Wymagana jedna mała litera.',
portMax: 'Maksymalny numer portu to 65535.',
endpoint: 'Wpisz prawidłowy punkt końcowy.',
endpoint: 'Wpisz poprawny adres.',
address: 'Wprowadź poprawny adres.',
addressNetmask: 'Wprowadź poprawny adres IP oraz maskę sieci.',
validPort: 'Wprowadź prawidłowy port.',
validCode: 'Kod powinien mieć 6 cyfr.',
allowedIps: 'Tylko poprawne adresy IP oraz domeny.',
Expand Down
47 changes: 35 additions & 12 deletions web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import './style.scss';

import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import ipaddr from 'ipaddr.js';
import { isNull, omit, omitBy } from 'lodash-es';
import { useEffect, useMemo, useRef, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form';
Expand All @@ -20,11 +21,7 @@ import { QueryKeys } from '../../../shared/queries';
import { Network } from '../../../shared/types';
import { titleCase } from '../../../shared/utils/titleCase';
import { trimObjectStrings } from '../../../shared/utils/trimObjectStrings.ts';
import {
validateIp,
validateIpOrDomain,
validateIpOrDomainList,
} from '../../../shared/validators';
import { validateIpOrDomain, validateIpOrDomainList } from '../../../shared/validators';
import { useNetworkPageStore } from '../hooks/useNetworkPageStore';

type FormFields = {
Expand Down Expand Up @@ -155,17 +152,43 @@ export const NetworkEditForm = () => {
if (!netmaskPresent) {
return false;
}
const ipValid = validateIp(value, true);
if (ipValid) {
const host = value.split('.')[3].split('/')[0];
if (host === '0') return false;
const ipValid = ipaddr.isValidCIDR(value);
if (!ipValid) {
return false;
}
const [address] = ipaddr.parseCIDR(value);
if (address.kind() === 'ipv6') {
const networkAddress = ipaddr.IPv6.networkAddressFromCIDR(value);
const broadcastAddress = ipaddr.IPv6.broadcastAddressFromCIDR(value);
if (
(address as ipaddr.IPv6).toNormalizedString() ===
networkAddress.toNormalizedString() ||
(address as ipaddr.IPv6).toNormalizedString() ===
broadcastAddress.toNormalizedString()
) {
return false;
}
} else {
const networkAddress = ipaddr.IPv4.networkAddressFromCIDR(value);
const broadcastAddress = ipaddr.IPv4.broadcastAddressFromCIDR(value);
if (
(address as ipaddr.IPv4).toNormalizedString() ===
networkAddress.toNormalizedString() ||
(address as ipaddr.IPv4).toNormalizedString() ===
broadcastAddress.toNormalizedString()
) {
return false;
}
}
return ipValid;
}, LL.form.error.address()),
}, LL.form.error.addressNetmask()),
endpoint: z
.string()
.min(1, LL.form.error.required())
.refine((val) => validateIpOrDomain(val), LL.form.error.endpoint()),
.refine(
(val) => validateIpOrDomain(val, false, true),
LL.form.error.endpoint(),
),
port: z
.number({
invalid_type_error: LL.form.error.required(),
Expand All @@ -179,7 +202,7 @@ export const NetworkEditForm = () => {
if (val === '' || !val) {
return true;
}
return validateIpOrDomainList(val, ',', true);
return validateIpOrDomainList(val, ',', false, true);
}, LL.form.error.allowedIps()),
allowed_groups: z.array(z.string().min(1, LL.form.error.minimumLength())),
mfa_enabled: z.boolean(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const SmtpSettingsForm = () => {
.string()
.min(1, LL.form.error.required())
.refine(
(val) => (!val ? true : validateIpOrDomain(val)),
(val) => (!val ? true : validateIpOrDomain(val, false, true)),
LL.form.error.endpoint(),
),
smtp_port: z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { QueryKeys } from '../../../../shared/queries';
import { ModifyNetworkRequest } from '../../../../shared/types';
import { titleCase } from '../../../../shared/utils/titleCase';
import { trimObjectStrings } from '../../../../shared/utils/trimObjectStrings.ts';
import { validateIp, validateIpOrDomainList } from '../../../../shared/validators';
import { validateIpOrDomainList, validateIPv4 } from '../../../../shared/validators';
import { useWizardStore } from '../../hooks/useWizardStore';

type FormInputs = ModifyNetworkRequest['network'];
Expand Down Expand Up @@ -91,7 +91,7 @@ export const WizardNetworkConfiguration = () => {
if (!netmaskPresent) {
return false;
}
const ipValid = validateIp(value, true);
const ipValid = validateIPv4(value, true);
if (ipValid) {
const host = value.split('.')[3].split('/')[0];
if (host === '0') return false;
Expand Down
3 changes: 0 additions & 3 deletions web/src/shared/patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,6 @@ export const patternValidUrl = new RegExp(
export const patternValidDomain =
/^(?:(?:(?:[a-zA-z\-]+)\:\/{1,3})?(?:[a-zA-Z0-9])(?:[a-zA-Z0-9\-\.]){1,61}(?:\.[a-zA-Z]{2,})+|\[(?:(?:(?:[a-fA-F0-9]){1,4})(?::(?:[a-fA-F0-9]){1,4}){7}|::1|::)\]|(?:(?:[0-9]{1,3})(?:\.[0-9]{1,3}){3}))(?:\:[0-9]{1,5})?$/;

export const patternValidIp =
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;

export const patternSafeUsernameCharacters = /^[a-zA-Z0-9]+[a-zA-Z0-9.\-_]+$/;

export const patternSafePasswordCharacters =
Expand Down
47 changes: 33 additions & 14 deletions web/src/shared/validators.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { patternValidDomain, patternValidIp } from './patterns';
import ipaddr from 'ipaddr.js';

import { patternValidDomain } from './patterns';

// Returns flase when invalid
export const validateIpOrDomain = (val: string, allowMask = false): boolean => {
return validateIp(val, allowMask) || patternValidDomain.test(val);
export const validateIpOrDomain = (
val: string,
allowMask = false,
allowIPv6 = false,
): boolean => {
return (
(allowIPv6 && validateIPv6(val, allowMask)) ||
validateIPv4(val, allowMask) ||
patternValidDomain.test(val)
);
};

// Returns flase when invalid
Expand All @@ -14,7 +24,7 @@ export const validateIpList = (
const trimed = val.replace(' ', '');
const split = trimed.split(splitWith);
for (const value of split) {
if (!validateIp(value, allowMasks)) {
if (!validateIPv4(value, allowMasks)) {
return false;
}
}
Expand All @@ -26,31 +36,40 @@ export const validateIpOrDomainList = (
val: string,
splitWith = ',',
allowMasks = false,
allowIPv6 = false,
): boolean => {
const trimed = val.replace(' ', '');
const split = trimed.split(splitWith);
for (const value of split) {
if (!validateIp(value, allowMasks) && !patternValidDomain.test(value)) {
console.log(allowIPv6 && !validateIPv6(value, allowMasks));
if (
!validateIPv4(value, allowMasks) &&
!patternValidDomain.test(value) &&
(!allowIPv6 || !validateIPv6(value, allowMasks))
) {
return false;
}
}
return true;
};

// Returns flase when invalid
export const validateIp = (ip: string, allowMask = false): boolean => {
export const validateIPv4 = (ip: string, allowMask = false): boolean => {
if (allowMask) {
if (ip.includes('/')) {
ipaddr.IPv4.isValidCIDR(ip);
}
}
return ipaddr.IPv4.isValid(ip);
};

export const validateIPv6 = (ip: string, allowMask = false): boolean => {
if (allowMask) {
if (ip.includes('/')) {
const split = ip.split('/');
if (split.length !== 2) return true;
const ipValid = patternValidIp.test(split[0]);
if (split[1] === '') return false;
const mask = Number(split[1]);
const maskValid = mask >= 0 && mask <= 32;
return ipValid && maskValid;
ipaddr.IPv6.isValidCIDR(ip);
}
}
return patternValidIp.test(ip);
return ipaddr.IPv6.isValid(ip);
};

export const validatePort = (val: string) => {
Expand Down

0 comments on commit 67b7522

Please sign in to comment.