From 40705c1bff937a08c6f009c93dd6cf001cd833d3 Mon Sep 17 00:00:00 2001 From: da Kai Date: Sun, 19 Apr 2026 23:11:22 +0200 Subject: [PATCH 01/11] add ipv6 support to ui --- .../systemexecute/systemexecute.component.ts | 14 ++- ui/src/app/shared/shared.module.ts | 21 +--- .../app/shared/utils/inet/inet.utils.spec.ts | 108 ++++++++++++++++++ ui/src/app/shared/utils/inet/inet.utils.ts | 67 +++++++++++ 4 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 ui/src/app/shared/utils/inet/inet.utils.spec.ts create mode 100644 ui/src/app/shared/utils/inet/inet.utils.ts diff --git a/ui/src/app/edge/settings/systemexecute/systemexecute.component.ts b/ui/src/app/edge/settings/systemexecute/systemexecute.component.ts index 0b7d0923b18..5b15b8ab38e 100644 --- a/ui/src/app/edge/settings/systemexecute/systemexecute.component.ts +++ b/ui/src/app/edge/settings/systemexecute/systemexecute.component.ts @@ -1,12 +1,13 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { FormBuilder, FormControl, FormGroup } from "@angular/forms"; +import { AbstractControl, FormBuilder, FormControl, FormGroup } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { FormlyFieldConfig, FormlyFormOptions } from "@ngx-formly/core"; import { TranslateService } from "@ngx-translate/core"; import { ComponentJsonApiRequest } from "src/app/shared/jsonrpc/request/componentJsonApiRequest"; import { ExecuteSystemCommandRequest } from "src/app/shared/jsonrpc/request/executeCommandRequest"; import { ExecuteSystemCommandResponse } from "src/app/shared/jsonrpc/response/executeSystemCommandResponse"; +import { InetUtils } from "src/app/shared/utils/inet/inet.utils"; import { Service, Utils, Websocket } from "../../../shared/shared"; type CommandFunction = (...args: (string | boolean | number)[]) => string; @@ -45,12 +46,13 @@ export class SystemExecuteComponent implements OnInit { key: "ip", type: "input", templateOptions: { - label: "IP-Address", placeholder: "192.168.0.1", required: true, pattern: /(\d{1,3}\.){3}\d{1,3}/, + label: "IP-Address / Hostname", placeholder: "127.0.0.1 / localhost", required: true, }, - validation: { - messages: { - pattern: (error, field: FormlyFieldConfig) => `"${field.formControl.value}" is not a valid IP Address`, - }, + validators: { + ip: { + expression: (c: AbstractControl) => InetUtils.isHostnameOrIp(c.value), + message: (error, field) => `${field.formControl?.value} is not a valid IP-Address or Hostname`, + } }, }], }, { diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index 9b04392cc75..66e7232dbd9 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -25,10 +25,10 @@ import { FormlyFieldModalComponent } from "./components/formly/formly-field-moda import { FormlyFieldNavigationComponent } from "./components/formly/formly-field-navigation/formly-field-navigation"; import { FormlyRangeTypeComponent } from "./components/formly/formly-field-range"; import { FormlyRadioTypeComponent } from "./components/formly/formly-radio/formly-radio"; -import { FormlySelectComponent } from "./components/formly/formly-select/formly-select"; -import { FormlySelectOptionsWithImageModalComponent } from "./components/formly/formly-select/formly-select-with-image-modal/select-with-image-modal.component"; import { FormlySelectFieldModalComponent } from "./components/formly/formly-select-field-modal.component"; import { FormlySelectFieldExtendedWrapperComponent } from "./components/formly/formly-select-field.extended"; +import { FormlySelectComponent } from "./components/formly/formly-select/formly-select"; +import { FormlySelectOptionsWithImageModalComponent } from "./components/formly/formly-select/formly-select-with-image-modal/select-with-image-modal.component"; import { FormlyFieldWithLoadingAnimationComponent } from "./components/formly/formly-skeleton-wrapper"; import { FormlyTariffTableTypeComponent } from "./components/formly/formly-tariff-table/formly-custom-tariff-table"; import { FormlyFieldCheckboxWithLabelComponent } from "./components/formly/help-popover-label-with-description-and-checkbox/help-popover-label-with-description-and-checkbox"; @@ -74,23 +74,6 @@ export function registerTranslateExtension(translate: TranslateService) { }; } - -export function IpValidator(control: FormControl): ValidationErrors { - return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(control.value) ? null : { "ip": true }; -} - -export function SubnetmaskValidator(control: FormControl): ValidationErrors { - return /^(255)\.(0|128|192|224|240|248|252|254|255)\.(0|128|192|224|240|248|252|254|255)\.(0|128|192|224|240|248|252|254|255)/.test(control.value) ? null : { "subnetmask": true }; -} - -export function IpValidatorMessage(err, field: FormlyFieldConfig) { - return `"${field.formControl.value}" is not a valid IP Address`; -} - -export function SubnetmaskValidatorMessage(err, field: FormlyFieldConfig) { - return `"${field.formControl.value}" is not a valid Subnetmask`; -} - export function PersonNameProhibitedCharactersValidator(control: FormControl): ValidationErrors { // https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/userprofile/validator/PersonNameProhibitedCharactersValidator.java const INVALID_CHARACTERS: string[] = [ diff --git a/ui/src/app/shared/utils/inet/inet.utils.spec.ts b/ui/src/app/shared/utils/inet/inet.utils.spec.ts new file mode 100644 index 00000000000..c1cb8c6febc --- /dev/null +++ b/ui/src/app/shared/utils/inet/inet.utils.spec.ts @@ -0,0 +1,108 @@ +import { InetUtils } from "./inet.utils"; + +const VALID_IPV4 = "192.168.0.1"; +const VALID_IPV6 = "2001:0db8:1234:5678:9999::ABCD:EFff"; +const VALID_HOSTNAME = "openems.io"; + +describe("InetUtils", () => { + + it("#isIpv4", () => { + expect(InetUtils.isIPv4(VALID_IPV4)).toBeTrue(); + expect(InetUtils.isIPv4(VALID_IPV6)).toBeFalse(); + expect(InetUtils.isIPv4(VALID_HOSTNAME)).toBeFalse(); + + expect(InetUtils.isIPv4("001.001.001.001")).toBeTrue(); + + expect(InetUtils.isIPv4("1.1.1.1.1")).toBeFalse(); + expect(InetUtils.isIPv4("1.1.1")).toBeFalse(); + expect(InetUtils.isIPv4("10.0.0.256")).toBeFalse(); + expect(InetUtils.isIPv4("1.0.0.1111")).toBeFalse(); + expect(InetUtils.isIPv4("1:1:1:1")).toBeFalse(); + }); + + it("#isIpv6", () => { + expect(InetUtils.isIPv6(VALID_IPV4)).toBeFalse(); + expect(InetUtils.isIPv6(VALID_IPV6)).toBeTrue(); + expect(InetUtils.isIPv6(VALID_HOSTNAME)).toBeFalse(); + + expect(InetUtils.isIPv6("::")).toBeTrue(); + expect(InetUtils.isIPv6("::1")).toBeTrue(); + expect(InetUtils.isIPv6("1::1")).toBeTrue(); + + expect(InetUtils.isIPv6("1:1:1:1")).toBeFalse(); + expect(InetUtils.isIPv6("1::defg")).toBeFalse(); + expect(InetUtils.isIPv6("1111::2222::3333")).toBeFalse(); + }); + + it("#isIp", () => { + expect(InetUtils.isIP(VALID_IPV4)).toBe(InetUtils.IpType.IPv4); + expect(InetUtils.isIP(VALID_IPV6)).toBe(InetUtils.IpType.IPv6); + expect(InetUtils.isIP(VALID_HOSTNAME)).toBe(InetUtils.IpType.None); + + expect(InetUtils.isIP("001.001.001.001")).toBe(InetUtils.IpType.IPv4); + expect(InetUtils.isIP("::")).toBe(InetUtils.IpType.IPv6); + expect(InetUtils.isIP("::1")).toBe(InetUtils.IpType.IPv6); + expect(InetUtils.isIP("1::1")).toBe(InetUtils.IpType.IPv6); + + expect(InetUtils.isIP("localhost")).toBe(InetUtils.IpType.None); + expect(InetUtils.isIP("")).toBe(InetUtils.IpType.None); + expect(InetUtils.isIP(null)).toBe(InetUtils.IpType.None); + }); + + it("#isValidIp", () => { + expect(InetUtils.isValidIP(VALID_IPV4)).toBeTrue(); + expect(InetUtils.isValidIP(VALID_IPV6)).toBeTrue(); + expect(InetUtils.isValidIP(VALID_HOSTNAME)).toBeFalse(); + + expect(InetUtils.isValidIP("001.001.001.001")).toBeTrue(); + expect(InetUtils.isValidIP("::")).toBeTrue(); + expect(InetUtils.isValidIP("::1")).toBeTrue(); + expect(InetUtils.isValidIP("1::1")).toBeTrue(); + + expect(InetUtils.isValidIP("localhost")).toBeFalse(); + expect(InetUtils.isValidIP("")).toBeFalse(); + expect(InetUtils.isValidIP(null)).toBeFalse(); + }); + + it("#checkHostname", () => { + expect(InetUtils.isHostname(VALID_IPV4)).toBeFalse(); + expect(InetUtils.isHostname(VALID_IPV6)).toBeFalse(); + expect(InetUtils.isHostname(VALID_HOSTNAME)).toBeTrue(); + + expect(InetUtils.isHostname("localhost")).toBeTrue(); + + expect(InetUtils.isHostname("1.test")).toBeTrue(); + expect(InetUtils.isHostname("1.1")).toBeFalse(); + expect(InetUtils.isHostname("local-test")).toBeTrue(); + expect(InetUtils.isHostname("-localtest")).toBeFalse(); + expect(InetUtils.isHostname("local.-test")).toBeFalse(); + expect(InetUtils.isHostname("a-b.c")).toBeTrue(); + expect(InetUtils.isHostname("a@b.c")).toBeFalse(); + expect(InetUtils.isHostname("openems.io.")).toBeTrue(); + expect(InetUtils.isHostname(".openems.io")).toBeFalse(); + expect(InetUtils.isHostname("1.1.1.1")).toBeFalse(); + expect(InetUtils.isHostname("256.256.256.256")).toBeFalse(); + expect(InetUtils.isHostname("1:1:1:1")).toBeFalse(); + }); + + it("#checkHostnameOrIp", () => { + expect(InetUtils.isHostnameOrIp(VALID_IPV4)).toBeTrue(); + expect(InetUtils.isHostnameOrIp(VALID_IPV6)).toBeTrue(); + expect(InetUtils.isHostnameOrIp(VALID_HOSTNAME)).toBeTrue(); + + // Localhost/Loopback + expect(InetUtils.isHostnameOrIp("127.0.0.1")).toBeTrue(); + expect(InetUtils.isHostnameOrIp("::1")).toBeTrue(); + expect(InetUtils.isHostnameOrIp("localhost")).toBeTrue(); + + // Any + expect(InetUtils.isHostnameOrIp("0.0.0.0")).toBeTrue(); + expect(InetUtils.isHostnameOrIp("::")).toBeTrue(); + + expect(InetUtils.isHostnameOrIp("a@b.c")).toBeFalse(); + expect(InetUtils.isHostnameOrIp("openems::io")).toBeFalse(); + expect(InetUtils.isHostnameOrIp(".openems.io")).toBeFalse(); + expect(InetUtils.isHostnameOrIp("1:1:1:1")).toBeFalse(); + }); + +}); diff --git a/ui/src/app/shared/utils/inet/inet.utils.ts b/ui/src/app/shared/utils/inet/inet.utils.ts new file mode 100644 index 00000000000..9a165b7afb1 --- /dev/null +++ b/ui/src/app/shared/utils/inet/inet.utils.ts @@ -0,0 +1,67 @@ +export namespace InetUtils { + export enum IpType { + None = 0, + IPv4 = 4, + IPv6 = 6, + } + + export const IPV4_PATTERN: RegExp = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + export const IPV6_PATTERN: RegExp = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|:((:[0-9a-fA-F]{1,4}){1,7})|::|([0-9a-fA-F]{1,4}:){1}(:[0-9a-fA-F]{1,4}){1,6}|([0-9a-fA-F]{1,4}:){2}(:[0-9a-fA-F]{1,4}){1,5}|([0-9a-fA-F]{1,4}:){3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){6}:[0-9a-fA-F]{1,4})$/; + export const HOSTNAME_PATTERN: RegExp = /^([A-Za-z0-9][A-Za-z0-9-]*\.)*[A-Za-z][A-Za-z0-9-]*\.?$/; + + export function isIPv4(value: string): boolean { + return IPV4_PATTERN.test(value); + } + + export function isIPv6(value: string): boolean { + return IPV6_PATTERN.test(value); + } + + export function isIP(value: string): IpType { + if (isIPv4(value)) { + return IpType.IPv4; + } + if (isIPv6(value)) { + return IpType.IPv6; + } + return IpType.None; + } + + export function isValidIP(value: string): boolean { + return isIPv4(value) || isIPv6(value); + } + + /** + * Check if input string is a valid hostname according to RFC 1123 and RFC 952. + * As the syntax of Hostnames and IPv4-Addresses have a intersection, valid IPv4-Addresses are not considered hostnames. + * + * ```js + * InetUtils.isHostname('openems.io'); // returns true + * InetUtils.isHostname('localhost'); // returns true + * InetUtils.isHostname('127.0.0.1'); // returns false + * InetUtils.isHostname(''); // returns false + * ``` + * @param value to check for hostname + * @returns true if it is a valid hostename + */ + export function isHostname(value: string): boolean { + return HOSTNAME_PATTERN.test(value) && !isIPv4(value); + } + + /** + * Check if string represents a valid Hostname, IPv4 or IPv6. + * + * ```js + * InetUtils.isHostnameOrIp('openems.io'); // returns true + * InetUtils.isHostnameOrIp('localhost'); // returns true + * InetUtils.isHostnameOrIp('127.0.0.1'); // returns true + * InetUtils.isHostnameOrIp('::1'); // returns true + * InetUtils.isHostnameOrIp(''); // returns false + * ``` + * @param value te check + * @returns true if string is a valid Hostname, IPv4 or IPv6. + */ + export function isHostnameOrIp(value: string): boolean { + return isValidIP(value) || isHostname(value); + } +} From 3c96810b1fed2d7159feb8b0c6289180aa9a61f3 Mon Sep 17 00:00:00 2001 From: da Kai Date: Sun, 19 Apr 2026 23:18:37 +0200 Subject: [PATCH 02/11] linting --- .../edge/settings/systemexecute/systemexecute.component.ts | 2 +- ui/src/app/shared/shared.module.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/app/edge/settings/systemexecute/systemexecute.component.ts b/ui/src/app/edge/settings/systemexecute/systemexecute.component.ts index 5b15b8ab38e..a9224752b55 100644 --- a/ui/src/app/edge/settings/systemexecute/systemexecute.component.ts +++ b/ui/src/app/edge/settings/systemexecute/systemexecute.component.ts @@ -52,7 +52,7 @@ export class SystemExecuteComponent implements OnInit { ip: { expression: (c: AbstractControl) => InetUtils.isHostnameOrIp(c.value), message: (error, field) => `${field.formControl?.value} is not a valid IP-Address or Hostname`, - } + }, }, }], }, { diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index 66e7232dbd9..89b21e69822 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -25,10 +25,10 @@ import { FormlyFieldModalComponent } from "./components/formly/formly-field-moda import { FormlyFieldNavigationComponent } from "./components/formly/formly-field-navigation/formly-field-navigation"; import { FormlyRangeTypeComponent } from "./components/formly/formly-field-range"; import { FormlyRadioTypeComponent } from "./components/formly/formly-radio/formly-radio"; -import { FormlySelectFieldModalComponent } from "./components/formly/formly-select-field-modal.component"; -import { FormlySelectFieldExtendedWrapperComponent } from "./components/formly/formly-select-field.extended"; import { FormlySelectComponent } from "./components/formly/formly-select/formly-select"; import { FormlySelectOptionsWithImageModalComponent } from "./components/formly/formly-select/formly-select-with-image-modal/select-with-image-modal.component"; +import { FormlySelectFieldModalComponent } from "./components/formly/formly-select-field-modal.component"; +import { FormlySelectFieldExtendedWrapperComponent } from "./components/formly/formly-select-field.extended"; import { FormlyFieldWithLoadingAnimationComponent } from "./components/formly/formly-skeleton-wrapper"; import { FormlyTariffTableTypeComponent } from "./components/formly/formly-tariff-table/formly-custom-tariff-table"; import { FormlyFieldCheckboxWithLabelComponent } from "./components/formly/help-popover-label-with-description-and-checkbox/help-popover-label-with-description-and-checkbox"; From 6378e39dc528fbf41861b16c78b7b792db0d0621 Mon Sep 17 00:00:00 2001 From: da Kai Date: Mon, 20 Apr 2026 22:25:11 +0200 Subject: [PATCH 03/11] add SUBNET_MASK_PATTERN; rename ip to host --- .../systemexecute/systemexecute.component.ts | 45 ++++++++++--------- ui/src/app/shared/shared.module.ts | 17 +++++++ ui/src/app/shared/utils/inet/inet.utils.ts | 5 +++ 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/ui/src/app/edge/settings/systemexecute/systemexecute.component.ts b/ui/src/app/edge/settings/systemexecute/systemexecute.component.ts index a9224752b55..df1d9430112 100644 --- a/ui/src/app/edge/settings/systemexecute/systemexecute.component.ts +++ b/ui/src/app/edge/settings/systemexecute/systemexecute.component.ts @@ -13,7 +13,7 @@ import { Service, Utils, Websocket } from "../../../shared/shared"; type CommandFunction = (...args: (string | boolean | number)[]) => string; const COMMANDS: { [key: string]: CommandFunction; } = { - "ping": (ip: string) => `ping -c4 ${ip}`, + "ping": (host: string) => `ping -c4 ${host}`, "openems-restart": () => "which at || DEBIAN_FRONTEND=noninteractive apt-get -y install at; echo 'systemctl restart openems' | at now", }; @@ -43,13 +43,13 @@ export class SystemExecuteComponent implements OnInit { key: "ping", hideExpression: (model: any, formState: any) => this.model["predefined"] !== "ping", fieldGroup: [{ - key: "ip", + key: "host", type: "input", templateOptions: { - label: "IP-Address / Hostname", placeholder: "127.0.0.1 / localhost", required: true, + label: "Host", placeholder: "127.0.0.1 / localhost", required: true, }, validators: { - ip: { + host: { expression: (c: AbstractControl) => InetUtils.isHostnameOrIp(c.value), message: (error, field) => `${field.formControl?.value} is not a valid IP-Address or Hostname`, }, @@ -146,7 +146,7 @@ export class SystemExecuteComponent implements OnInit { const cmd = COMMANDS[m.predefined]; switch (m.predefined) { case "ping": - command = cmd(m.ping.ip); + command = cmd(m.ping.host); break; case "openems-restart": default: @@ -175,24 +175,25 @@ export class SystemExecuteComponent implements OnInit { command: command.value, }); - edge.sendRequest(this.websocket, - new ComponentJsonApiRequest({ - componentId: "_host", - payload: executeSystemCommandRequest, - })).then(response => { - const result = (response as ExecuteSystemCommandResponse).result; - this.loading = false; - if (result.stdout.length == 0) { - this.stdout = [""]; - } else { - this.stdout = result.stdout; - } - this.stderr = result.stderr; - - }).catch(reason => { - this.loading = false; - this.stderr = ["Error executing system command:", reason.error.message]; + const request = new ComponentJsonApiRequest({ + componentId: "_host", + payload: executeSystemCommandRequest, }); + edge.sendRequest(this.websocket, request) + .then(response => { + const result = (response as ExecuteSystemCommandResponse).result; + this.loading = false; + if (result.stdout.length == 0) { + this.stdout = [""]; + } else { + this.stdout = result.stdout; + } + this.stderr = result.stderr; + + }).catch(reason => { + this.loading = false; + this.stderr = ["Error executing system command:", reason.error.message]; + }); this.commandLogs.unshift(executeSystemCommandRequest); }); } diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index 89b21e69822..a40a0f8514a 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -58,6 +58,7 @@ import { RouteService } from "./service/route.service"; import { Service } from "./service/service"; import { Utils, Websocket } from "./shared"; import { Language } from "./type/language"; +import { InetUtils } from "./utils/inet/inet.utils"; export function registerTranslateExtension(translate: TranslateService) { @@ -74,6 +75,22 @@ export function registerTranslateExtension(translate: TranslateService) { }; } +export function IpValidator(control: FormControl): ValidationErrors { + return InetUtils.isIP(control.value) ? null : { "ip": true }; +} + +export function SubnetmaskValidator(control: FormControl): ValidationErrors { + return InetUtils.isSubnetMask(control.value) ? null : { "subnetmask": true }; +} + +export function IpValidatorMessage(err, field: FormlyFieldConfig) { + return `"${field.formControl.value}" is not a valid IP Address`; +} + +export function SubnetmaskValidatorMessage(err, field: FormlyFieldConfig) { + return `"${field.formControl.value}" is not a valid Subnetmask`; +} + export function PersonNameProhibitedCharactersValidator(control: FormControl): ValidationErrors { // https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/userprofile/validator/PersonNameProhibitedCharactersValidator.java const INVALID_CHARACTERS: string[] = [ diff --git a/ui/src/app/shared/utils/inet/inet.utils.ts b/ui/src/app/shared/utils/inet/inet.utils.ts index 9a165b7afb1..9963ba10466 100644 --- a/ui/src/app/shared/utils/inet/inet.utils.ts +++ b/ui/src/app/shared/utils/inet/inet.utils.ts @@ -5,6 +5,7 @@ export namespace InetUtils { IPv6 = 6, } + export const SUBNET_MASK_PATTERN: RegExp = /^((0|128|192|224|240|248|252|254|255)\.){3}(0|128|192|224|240|248|252|254|255)$/; export const IPV4_PATTERN: RegExp = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; export const IPV6_PATTERN: RegExp = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|:((:[0-9a-fA-F]{1,4}){1,7})|::|([0-9a-fA-F]{1,4}:){1}(:[0-9a-fA-F]{1,4}){1,6}|([0-9a-fA-F]{1,4}:){2}(:[0-9a-fA-F]{1,4}){1,5}|([0-9a-fA-F]{1,4}:){3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){6}:[0-9a-fA-F]{1,4})$/; export const HOSTNAME_PATTERN: RegExp = /^([A-Za-z0-9][A-Za-z0-9-]*\.)*[A-Za-z][A-Za-z0-9-]*\.?$/; @@ -17,6 +18,10 @@ export namespace InetUtils { return IPV6_PATTERN.test(value); } + export function isSubnetMask(value: string): boolean { + return SUBNET_MASK_PATTERN.test(value); + } + export function isIP(value: string): IpType { if (isIPv4(value)) { return IpType.IPv4; From 3ade2bc54f2322e247319c70398df6bfaa127419 Mon Sep 17 00:00:00 2001 From: da Kai Date: Tue, 21 Apr 2026 20:33:44 +0200 Subject: [PATCH 04/11] fix subnetmask pattern --- ui/src/app/shared/utils/inet/inet.utils.spec.ts | 12 ++++++++++++ ui/src/app/shared/utils/inet/inet.utils.ts | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ui/src/app/shared/utils/inet/inet.utils.spec.ts b/ui/src/app/shared/utils/inet/inet.utils.spec.ts index c1cb8c6febc..a8e3abac7ca 100644 --- a/ui/src/app/shared/utils/inet/inet.utils.spec.ts +++ b/ui/src/app/shared/utils/inet/inet.utils.spec.ts @@ -6,6 +6,18 @@ const VALID_HOSTNAME = "openems.io"; describe("InetUtils", () => { + it("#isSubnetMask", () => { + expect(InetUtils.isSubnetMask("255.255.255.255")).toBeTrue(); + expect(InetUtils.isSubnetMask("255.255.255.0")).toBeTrue(); + expect(InetUtils.isSubnetMask("255.255.192.0")).toBeTrue(); + expect(InetUtils.isSubnetMask("255.240.0.0")).toBeTrue(); + expect(InetUtils.isSubnetMask("0.0.0.0")).toBeTrue(); + + expect(InetUtils.isSubnetMask("255.255.255.200")).toBeFalse(); + expect(InetUtils.isSubnetMask("255.0.255.0")).toBeFalse(); + expect(InetUtils.isSubnetMask("255.192.255.192")).toBeFalse(); + }); + it("#isIpv4", () => { expect(InetUtils.isIPv4(VALID_IPV4)).toBeTrue(); expect(InetUtils.isIPv4(VALID_IPV6)).toBeFalse(); diff --git a/ui/src/app/shared/utils/inet/inet.utils.ts b/ui/src/app/shared/utils/inet/inet.utils.ts index 9963ba10466..3f85a09d52e 100644 --- a/ui/src/app/shared/utils/inet/inet.utils.ts +++ b/ui/src/app/shared/utils/inet/inet.utils.ts @@ -5,7 +5,7 @@ export namespace InetUtils { IPv6 = 6, } - export const SUBNET_MASK_PATTERN: RegExp = /^((0|128|192|224|240|248|252|254|255)\.){3}(0|128|192|224|240|248|252|254|255)$/; + export const SUBNET_MASK_PATTERN: RegExp = /^(0|128|192|224|240|248|252|254|255).0.0.0|255.(0|128|192|224|240|248|252|254|255).0.0|255.255.(0|128|192|224|240|248|252|254|255).0|255.255.255.(0|128|192|224|240|248|252|254|255)$/; export const IPV4_PATTERN: RegExp = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; export const IPV6_PATTERN: RegExp = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|:((:[0-9a-fA-F]{1,4}){1,7})|::|([0-9a-fA-F]{1,4}:){1}(:[0-9a-fA-F]{1,4}){1,6}|([0-9a-fA-F]{1,4}:){2}(:[0-9a-fA-F]{1,4}){1,5}|([0-9a-fA-F]{1,4}:){3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){6}:[0-9a-fA-F]{1,4})$/; export const HOSTNAME_PATTERN: RegExp = /^([A-Za-z0-9][A-Za-z0-9-]*\.)*[A-Za-z][A-Za-z0-9-]*\.?$/; From f1f9c274378e57e2d58cb33cada1b3d684b37414 Mon Sep 17 00:00:00 2001 From: da Kai Date: Tue, 21 Apr 2026 21:47:49 +0200 Subject: [PATCH 05/11] add checks for address + cidr combinations --- .../settings/network/network.component.ts | 4 +- .../app/shared/utils/inet/inet.utils.spec.ts | 34 ++++++++++ ui/src/app/shared/utils/inet/inet.utils.ts | 66 +++++++++++++++++-- 3 files changed, 98 insertions(+), 6 deletions(-) diff --git a/ui/src/app/edge/settings/network/network.component.ts b/ui/src/app/edge/settings/network/network.component.ts index 29e63aeb7a1..98065b4e303 100644 --- a/ui/src/app/edge/settings/network/network.component.ts +++ b/ui/src/app/edge/settings/network/network.component.ts @@ -9,6 +9,7 @@ import { PipeComponentsModule } from "src/app/shared/pipe/pipe.module"; import { LiveDataServiceProvider } from "src/app/shared/provider/live-data-service-provider"; import { LocaleProvider } from "src/app/shared/provider/locale-provider"; import { Role } from "src/app/shared/type/role"; +import { InetUtils } from "src/app/shared/utils/inet/inet.utils"; import { CommonUiModule } from "../../../shared/common-ui.module"; import { Edge, Service, Websocket } from "../../../shared/shared"; import { GetNetworkConfigRequest } from "./getNetworkConfigRequest"; @@ -42,7 +43,6 @@ export class NetworkComponent implements OnInit { public edge: Edge | null = null; protected forms: InterfaceForm[] = []; - protected ipRegex: RegExp = /^(?: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]?)){3}\/(?:3[0-2]|[0-2]?[0-9])$/; constructor( private translate: TranslateService, @@ -160,7 +160,7 @@ export class NetworkComponent implements OnInit { if (iface.model.addressesList) { for (const addr of iface.model.addressesList) { - if (!this.ipRegex.test(addr)) { + if (InetUtils.isValidNetworkAddress(addr)) { this.service.toast(this.translate.instant("EDGE.NETWORK.VALID_ADDRESS_WARNING"), "danger"); return []; } diff --git a/ui/src/app/shared/utils/inet/inet.utils.spec.ts b/ui/src/app/shared/utils/inet/inet.utils.spec.ts index a8e3abac7ca..277d3272637 100644 --- a/ui/src/app/shared/utils/inet/inet.utils.spec.ts +++ b/ui/src/app/shared/utils/inet/inet.utils.spec.ts @@ -16,6 +16,7 @@ describe("InetUtils", () => { expect(InetUtils.isSubnetMask("255.255.255.200")).toBeFalse(); expect(InetUtils.isSubnetMask("255.0.255.0")).toBeFalse(); expect(InetUtils.isSubnetMask("255.192.255.192")).toBeFalse(); + expect(InetUtils.isSubnetMask(null)).toBeFalse(); }); it("#isIpv4", () => { @@ -95,6 +96,7 @@ describe("InetUtils", () => { expect(InetUtils.isHostname("1.1.1.1")).toBeFalse(); expect(InetUtils.isHostname("256.256.256.256")).toBeFalse(); expect(InetUtils.isHostname("1:1:1:1")).toBeFalse(); + expect(InetUtils.isHostname(null)).toBeFalse(); }); it("#checkHostnameOrIp", () => { @@ -115,6 +117,38 @@ describe("InetUtils", () => { expect(InetUtils.isHostnameOrIp("openems::io")).toBeFalse(); expect(InetUtils.isHostnameOrIp(".openems.io")).toBeFalse(); expect(InetUtils.isHostnameOrIp("1:1:1:1")).toBeFalse(); + expect(InetUtils.isHostnameOrIp(null)).toBeFalse(); + }); + + it("#isNetworkAddress", () => { + expect(InetUtils.isNetworkAddress("0.0.0.0/0")).toBe(InetUtils.IpType.IPv4); + expect(InetUtils.isNetworkAddress("1.1.1.1/24")).toBe(InetUtils.IpType.IPv4); + expect(InetUtils.isNetworkAddress("1::1/64")).toBe(InetUtils.IpType.IPv6);; + expect(InetUtils.isNetworkAddress("::/0")).toBe(InetUtils.IpType.IPv6); + + expect(InetUtils.isNetworkAddress("1.1.1.1/64")).toBe(InetUtils.IpType.None); + expect(InetUtils.isNetworkAddress("1.1.1.1/test")).toBe(InetUtils.IpType.None); + expect(InetUtils.isNetworkAddress("1::1/200")).toBe(InetUtils.IpType.None); + expect(InetUtils.isNetworkAddress("1.1.1.1/4/8")).toBe(InetUtils.IpType.None); + expect(InetUtils.isNetworkAddress("1.1.1.1")).toBe(InetUtils.IpType.None); + expect(InetUtils.isNetworkAddress("localhost")).toBe(InetUtils.IpType.None); + expect(InetUtils.isNetworkAddress(null)).toBe(InetUtils.IpType.None); + }); + + + it("#isValidNetworkAddress", () => { + expect(InetUtils.isValidNetworkAddress("0.0.0.0/0")).toBeTrue(); + expect(InetUtils.isValidNetworkAddress("1.1.1.1/24")).toBeTrue(); + expect(InetUtils.isValidNetworkAddress("1::1/64")).toBeTrue(); + expect(InetUtils.isValidNetworkAddress("::/0")).toBeTrue(); + + expect(InetUtils.isValidNetworkAddress("1.1.1.1/64")).toBeFalse(); + expect(InetUtils.isValidNetworkAddress("1.1.1.1/test")).toBeFalse(); + expect(InetUtils.isValidNetworkAddress("1::1/200")).toBeFalse(); + expect(InetUtils.isValidNetworkAddress("1.1.1.1/4/8")).toBeFalse(); + expect(InetUtils.isValidNetworkAddress("1.1.1.1")).toBeFalse(); + expect(InetUtils.isValidNetworkAddress("localhost")).toBeFalse(); + expect(InetUtils.isValidNetworkAddress(null)).toBeFalse(); }); }); diff --git a/ui/src/app/shared/utils/inet/inet.utils.ts b/ui/src/app/shared/utils/inet/inet.utils.ts index 3f85a09d52e..b3b29849c95 100644 --- a/ui/src/app/shared/utils/inet/inet.utils.ts +++ b/ui/src/app/shared/utils/inet/inet.utils.ts @@ -11,11 +11,11 @@ export namespace InetUtils { export const HOSTNAME_PATTERN: RegExp = /^([A-Za-z0-9][A-Za-z0-9-]*\.)*[A-Za-z][A-Za-z0-9-]*\.?$/; export function isIPv4(value: string): boolean { - return IPV4_PATTERN.test(value); + return value != null && IPV4_PATTERN.test(value); } export function isIPv6(value: string): boolean { - return IPV6_PATTERN.test(value); + return value != null && IPV6_PATTERN.test(value); } export function isSubnetMask(value: string): boolean { @@ -36,6 +36,30 @@ export namespace InetUtils { return isIPv4(value) || isIPv6(value); } + export function isNetworkAddress(value: string): IpType { + if (value === null || value.length == 0) return IpType.None; + + const parts: string[] = value.split('/'); + if (parts.length != 2) return IpType.None; + + const cidrNum: number = Number.parseInt(parts[1], 10); + if (Number.isNaN(cidrNum)) return IpType.None; + + const ipType = isIP(parts[0]) + if (ipType === IpType.IPv4 && isValidIPv4Cidr(cidrNum)) { + return IpType.IPv4; + } + if (ipType === IpType.IPv6 && isValidIPv6Cidr(cidrNum)) { + return IpType.IPv6; + } + return IpType.None; + } + + export function isValidNetworkAddress(value: string): boolean { + const type = isNetworkAddress(value); + return type === IpType.IPv4 || type === IpType.IPv6; + } + /** * Check if input string is a valid hostname according to RFC 1123 and RFC 952. * As the syntax of Hostnames and IPv4-Addresses have a intersection, valid IPv4-Addresses are not considered hostnames. @@ -50,7 +74,7 @@ export namespace InetUtils { * @returns true if it is a valid hostename */ export function isHostname(value: string): boolean { - return HOSTNAME_PATTERN.test(value) && !isIPv4(value); + return value != null && HOSTNAME_PATTERN.test(value) && !isIPv4(value); } /** @@ -63,10 +87,44 @@ export namespace InetUtils { * InetUtils.isHostnameOrIp('::1'); // returns true * InetUtils.isHostnameOrIp(''); // returns false * ``` - * @param value te check + * @param value to check * @returns true if string is a valid Hostname, IPv4 or IPv6. */ export function isHostnameOrIp(value: string): boolean { return isValidIP(value) || isHostname(value); } + + /** + * Check if number is a valid CIDR. + * + * + * ```js + * InetUtils.isValidIPv4Cidr(24); // returns true + * InetUtils.isValidIPv4Cidr(-1); // returns false + * InetUtils.isValidIPv4Cidr(100); // returns false + * InetUtils.isValidIPv4Cidr(null); // returns false + * ``` + * @param value to check + * @returns true if valid ipv4 CIDR + */ + export function isValidIPv4Cidr(value: number): boolean { + return Number.isFinite(value) && value >= 0 && value <= 32; + } + + /** + * Check if number is a valid CIDR. + * + * + * ```js + * InetUtils.isValidIPv6Cidr(24); // returns true + * InetUtils.isValidIPv6Cidr(-1); // returns false + * InetUtils.isValidIPv6Cidr(200); // returns false + * InetUtils.isValidIPv6Cidr(null); // returns false + * ``` + * @param value to check + * @returns true if valid ipv6 CIDR + */ + export function isValidIPv6Cidr(value: number): boolean { + return Number.isFinite(value) && value >= 0 && value <= 128; + } } From 30cc4630ee7f8e2087d6f063374c9192f9d253ef Mon Sep 17 00:00:00 2001 From: da Kai Date: Tue, 21 Apr 2026 21:53:11 +0200 Subject: [PATCH 06/11] add doc --- ui/src/app/shared/utils/inet/inet.utils.ts | 60 ++++++++++++++++++---- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/ui/src/app/shared/utils/inet/inet.utils.ts b/ui/src/app/shared/utils/inet/inet.utils.ts index b3b29849c95..c32a0fe7a32 100644 --- a/ui/src/app/shared/utils/inet/inet.utils.ts +++ b/ui/src/app/shared/utils/inet/inet.utils.ts @@ -10,18 +10,42 @@ export namespace InetUtils { export const IPV6_PATTERN: RegExp = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|:((:[0-9a-fA-F]{1,4}){1,7})|::|([0-9a-fA-F]{1,4}:){1}(:[0-9a-fA-F]{1,4}){1,6}|([0-9a-fA-F]{1,4}:){2}(:[0-9a-fA-F]{1,4}){1,5}|([0-9a-fA-F]{1,4}:){3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){6}:[0-9a-fA-F]{1,4})$/; export const HOSTNAME_PATTERN: RegExp = /^([A-Za-z0-9][A-Za-z0-9-]*\.)*[A-Za-z][A-Za-z0-9-]*\.?$/; + /** + * Checks whether a string is a valid IPv4 address. + * + * @param value the input string + * @returns true if the value is a valid IPv4 address + */ export function isIPv4(value: string): boolean { return value != null && IPV4_PATTERN.test(value); } + /** + * Checks whether a string is a valid IPv6 address. + * + * @param value the input string + * @returns true if the value is a valid IPv6 address + */ export function isIPv6(value: string): boolean { return value != null && IPV6_PATTERN.test(value); } + /** + * Checks whether a string is a valid IPv4 subnet mask. + * + * @param value the input string + * @returns true if the value is a valid subnet mask + */ export function isSubnetMask(value: string): boolean { return SUBNET_MASK_PATTERN.test(value); } + /** + * Detects whether a string is IPv4, IPv6 or neither. + * + * @param value the input string + * @returns the detected IP type + */ export function isIP(value: string): IpType { if (isIPv4(value)) { return IpType.IPv4; @@ -32,20 +56,32 @@ export namespace InetUtils { return IpType.None; } + /** + * Checks whether a string is either a valid IPv4 or IPv6 address. + * + * @param value the input string + * @returns true if the value is a valid IP address + */ export function isValidIP(value: string): boolean { return isIPv4(value) || isIPv6(value); } + /** + * Checks whether a string is a valid network address in CIDR notation. + * + * @param value the input string in the format "address/prefix" + * @returns the detected network address type + */ export function isNetworkAddress(value: string): IpType { - if (value === null || value.length == 0) return IpType.None; + if (value === null || value.length == 0) {return IpType.None;} - const parts: string[] = value.split('/'); - if (parts.length != 2) return IpType.None; + const parts: string[] = value.split("/"); + if (parts.length != 2) {return IpType.None;} const cidrNum: number = Number.parseInt(parts[1], 10); - if (Number.isNaN(cidrNum)) return IpType.None; + if (Number.isNaN(cidrNum)) {return IpType.None;} - const ipType = isIP(parts[0]) + const ipType = isIP(parts[0]); if (ipType === IpType.IPv4 && isValidIPv4Cidr(cidrNum)) { return IpType.IPv4; } @@ -55,6 +91,12 @@ export namespace InetUtils { return IpType.None; } + /** + * Checks whether a string is a valid IPv4 or IPv6 network address in CIDR notation. + * + * @param value the input string + * @returns true if the value is a valid network address + */ export function isValidNetworkAddress(value: string): boolean { const type = isNetworkAddress(value); return type === IpType.IPv4 || type === IpType.IPv6; @@ -96,8 +138,8 @@ export namespace InetUtils { /** * Check if number is a valid CIDR. - * - * + * + * * ```js * InetUtils.isValidIPv4Cidr(24); // returns true * InetUtils.isValidIPv4Cidr(-1); // returns false @@ -113,8 +155,8 @@ export namespace InetUtils { /** * Check if number is a valid CIDR. - * - * + * + * * ```js * InetUtils.isValidIPv6Cidr(24); // returns true * InetUtils.isValidIPv6Cidr(-1); // returns false From 070274c00c1d7777835cf978aa39e4b70b54d8aa Mon Sep 17 00:00:00 2001 From: da Kai Date: Tue, 21 Apr 2026 22:04:32 +0200 Subject: [PATCH 07/11] quickfix --- ui/src/app/edge/settings/network/network.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/edge/settings/network/network.component.ts b/ui/src/app/edge/settings/network/network.component.ts index 98065b4e303..9ef1980a65d 100644 --- a/ui/src/app/edge/settings/network/network.component.ts +++ b/ui/src/app/edge/settings/network/network.component.ts @@ -160,7 +160,7 @@ export class NetworkComponent implements OnInit { if (iface.model.addressesList) { for (const addr of iface.model.addressesList) { - if (InetUtils.isValidNetworkAddress(addr)) { + if (!InetUtils.isValidNetworkAddress(addr)) { this.service.toast(this.translate.instant("EDGE.NETWORK.VALID_ADDRESS_WARNING"), "danger"); return []; } From 5badde3b2492e1661bf62fb4c2b7c50a4b917c36 Mon Sep 17 00:00:00 2001 From: da Kai Date: Tue, 21 Apr 2026 22:29:02 +0200 Subject: [PATCH 08/11] network settings ipv4 --- ui/src/app/edge/settings/network/network.component.ts | 2 +- ui/src/assets/i18n/cz.json | 2 +- ui/src/assets/i18n/de.json | 2 +- ui/src/assets/i18n/en.json | 2 +- ui/src/assets/i18n/es.json | 2 +- ui/src/assets/i18n/fr.json | 2 +- ui/src/assets/i18n/nl.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ui/src/app/edge/settings/network/network.component.ts b/ui/src/app/edge/settings/network/network.component.ts index 9ef1980a65d..98196777773 100644 --- a/ui/src/app/edge/settings/network/network.component.ts +++ b/ui/src/app/edge/settings/network/network.component.ts @@ -160,7 +160,7 @@ export class NetworkComponent implements OnInit { if (iface.model.addressesList) { for (const addr of iface.model.addressesList) { - if (!InetUtils.isValidNetworkAddress(addr)) { + if (InetUtils.isNetworkAddress(addr) !== InetUtils.IpType.IPv4) { this.service.toast(this.translate.instant("EDGE.NETWORK.VALID_ADDRESS_WARNING"), "danger"); return []; } diff --git a/ui/src/assets/i18n/cz.json b/ui/src/assets/i18n/cz.json index a561b899b7b..0d38e95b0f4 100644 --- a/ui/src/assets/i18n/cz.json +++ b/ui/src/assets/i18n/cz.json @@ -365,7 +365,7 @@ "MANDATORY_FIELDS": "Vyplňte prosím povinná pole", "SUBNETMASK": "Maska podsítě", "SUCCESS_UPDATE": "Konfigurace sítě pro doménu byla úspěšně aktualizována", - "VALID_ADDRESS_WARNING": "V části „Přidat statické IP adresy“ zadejte platnou adresu IP se síťovou maskou, např. 10.4.1.6/24" + "VALID_ADDRESS_WARNING": "V části „Přidat statické IP adresy“ zadejte platnou adresu IPv4 se síťovou maskou, např. 10.4.1.6/24" }, "SERVICE": { "CELL": { diff --git a/ui/src/assets/i18n/de.json b/ui/src/assets/i18n/de.json index 501d2bb7433..2080c1d4013 100644 --- a/ui/src/assets/i18n/de.json +++ b/ui/src/assets/i18n/de.json @@ -568,7 +568,7 @@ "SUBMIT": "Anwenden", "SUBNETMASK": "Subnetzmaske", "SUCCESS_UPDATE": "Netzwerkkonfiguration erfolgreich aktualisiert für", - "VALID_ADDRESS_WARNING": "Geben Sie unter \"Statische IP-Adressen hinzufügen\" eine gültige IP-Adresse mit Netzmaske ein, z. B. 10.4.1.6/24" + "VALID_ADDRESS_WARNING": "Geben Sie unter \"Statische IP-Adressen hinzufügen\" eine gültige IPv4-Adresse mit Netzmaske ein, z. B. 10.4.1.6/24" }, "SERVICE": { "CELL": { diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index 5d06fc93ac9..04e12f8ed18 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -579,7 +579,7 @@ "SUBMIT": "Submit", "SUBNETMASK": "Subnetmask", "SUCCESS_UPDATE": "Successfully updated network configuration for", - "VALID_ADDRESS_WARNING": "In 'Add static IP addresses', Please enter valid ip address with subnetmask e.g. 10.4.1.6/24" + "VALID_ADDRESS_WARNING": "In 'Add static IP addresses', Please enter valid IPv4 address with subnetmask e.g. 10.4.1.6/24" }, "SERVICE": { "CELL": { diff --git a/ui/src/assets/i18n/es.json b/ui/src/assets/i18n/es.json index e0ddf929df6..be96c80f0a7 100644 --- a/ui/src/assets/i18n/es.json +++ b/ui/src/assets/i18n/es.json @@ -362,7 +362,7 @@ "MANDATORY_FIELDS": "Por favor complete los campos obligatorios", "SUBNETMASK": "Máscara de subred", "SUCCESS_UPDATE": "Configuración de red actualizada con éxito para", - "VALID_ADDRESS_WARNING": "En 'Agregar direcciones IP estáticas', ingrese una dirección IP válida con máscara de red, p. 10.4.1.6/24" + "VALID_ADDRESS_WARNING": "En 'Agregar direcciones IP estáticas', ingrese una dirección IPv4 válida con máscara de red, p. 10.4.1.6/24" }, "SERVICE": { "CELL": { diff --git a/ui/src/assets/i18n/fr.json b/ui/src/assets/i18n/fr.json index 65cc96cdbb5..2ab472e5678 100644 --- a/ui/src/assets/i18n/fr.json +++ b/ui/src/assets/i18n/fr.json @@ -359,7 +359,7 @@ "MANDATORY_FIELDS": "Merci de remplir les champs obligatoires", "SUBNETMASK": "Masque de sous-réseau", "SUCCESS_UPDATE": "Configuration réseau mise à jour avec succès pour", - "VALID_ADDRESS_WARNING": "Dans 'Ajouter des adresses IP statiques', veuillez entrer une adresse IP valide avec un masque de réseau, par ex. 10.4.1.6/24" + "VALID_ADDRESS_WARNING": "Dans 'Ajouter des adresses IP statiques', veuillez entrer une adresse IPv4 valide avec un masque de réseau, par ex. 10.4.1.6/24" }, "SERVICE": { "CELL": { diff --git a/ui/src/assets/i18n/nl.json b/ui/src/assets/i18n/nl.json index 12b1789cbf4..60df8be9b7c 100644 --- a/ui/src/assets/i18n/nl.json +++ b/ui/src/assets/i18n/nl.json @@ -361,7 +361,7 @@ "MANDATORY_FIELDS": "Gelieve de verplichte velden in te vullen", "SUBNETMASK": "Subnetmasker", "SUCCESS_UPDATE": "Netwerkconfiguratie geüpdatet voor:", - "VALID_ADDRESS_WARNING": "Voer bij 'Statische IP-adressen toevoegen' een geldig ip-adres in met netmasker, b.v. 10.4.1.6/24" + "VALID_ADDRESS_WARNING": "Voer bij 'Statische IP-adressen toevoegen' een geldig IPv4-adres in met netmasker, b.v. 10.4.1.6/24" }, "SERVICE": { "CELL": { From 0ef192ecbec2e5732d7c398d46456a3720d8f548 Mon Sep 17 00:00:00 2001 From: da Kai Date: Wed, 22 Apr 2026 21:29:24 +0200 Subject: [PATCH 09/11] keep validation at ipv4; fix subnetmask pattern --- ui/src/app/shared/shared.module.ts | 12 ++++++------ ui/src/app/shared/utils/inet/inet.utils.spec.ts | 3 ++- ui/src/app/shared/utils/inet/inet.utils.ts | 10 +++++----- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/ui/src/app/shared/shared.module.ts b/ui/src/app/shared/shared.module.ts index a40a0f8514a..35a6cc89357 100644 --- a/ui/src/app/shared/shared.module.ts +++ b/ui/src/app/shared/shared.module.ts @@ -75,16 +75,16 @@ export function registerTranslateExtension(translate: TranslateService) { }; } -export function IpValidator(control: FormControl): ValidationErrors { - return InetUtils.isIP(control.value) ? null : { "ip": true }; +export function Ipv4Validator(control: FormControl): ValidationErrors { + return InetUtils.isIPv4(control.value) ? null : { "ip": true }; } export function SubnetmaskValidator(control: FormControl): ValidationErrors { return InetUtils.isSubnetMask(control.value) ? null : { "subnetmask": true }; } -export function IpValidatorMessage(err, field: FormlyFieldConfig) { - return `"${field.formControl.value}" is not a valid IP Address`; +export function Ipv4ValidatorMessage(err, field: FormlyFieldConfig) { + return `"${field.formControl.value}" is not a valid IPv4-Address`; } export function SubnetmaskValidatorMessage(err, field: FormlyFieldConfig) { @@ -148,12 +148,12 @@ export function PersonNameProhibitedCharactersValidator(control: FormControl): V { name: "range", component: FormlyRangeTypeComponent }, ], validators: [ - { name: "ip", validation: IpValidator }, + { name: "ip", validation: Ipv4Validator }, { name: "subnetmask", validation: SubnetmaskValidator }, { name: "person-name-prohibited-characters", validation: PersonNameProhibitedCharactersValidator }, ], validationMessages: [ - { name: "ip", message: IpValidatorMessage }, + { name: "ip", message: Ipv4ValidatorMessage }, { name: "subnetmask", message: SubnetmaskValidatorMessage }, ], }), diff --git a/ui/src/app/shared/utils/inet/inet.utils.spec.ts b/ui/src/app/shared/utils/inet/inet.utils.spec.ts index 277d3272637..20698d69496 100644 --- a/ui/src/app/shared/utils/inet/inet.utils.spec.ts +++ b/ui/src/app/shared/utils/inet/inet.utils.spec.ts @@ -11,11 +11,12 @@ describe("InetUtils", () => { expect(InetUtils.isSubnetMask("255.255.255.0")).toBeTrue(); expect(InetUtils.isSubnetMask("255.255.192.0")).toBeTrue(); expect(InetUtils.isSubnetMask("255.240.0.0")).toBeTrue(); - expect(InetUtils.isSubnetMask("0.0.0.0")).toBeTrue(); + expect(InetUtils.isSubnetMask("128.0.0.0")).toBeTrue(); expect(InetUtils.isSubnetMask("255.255.255.200")).toBeFalse(); expect(InetUtils.isSubnetMask("255.0.255.0")).toBeFalse(); expect(InetUtils.isSubnetMask("255.192.255.192")).toBeFalse(); + expect(InetUtils.isSubnetMask("0.0.0.0")).toBeFalse(); expect(InetUtils.isSubnetMask(null)).toBeFalse(); }); diff --git a/ui/src/app/shared/utils/inet/inet.utils.ts b/ui/src/app/shared/utils/inet/inet.utils.ts index c32a0fe7a32..25b49bd872a 100644 --- a/ui/src/app/shared/utils/inet/inet.utils.ts +++ b/ui/src/app/shared/utils/inet/inet.utils.ts @@ -5,7 +5,7 @@ export namespace InetUtils { IPv6 = 6, } - export const SUBNET_MASK_PATTERN: RegExp = /^(0|128|192|224|240|248|252|254|255).0.0.0|255.(0|128|192|224|240|248|252|254|255).0.0|255.255.(0|128|192|224|240|248|252|254|255).0|255.255.255.(0|128|192|224|240|248|252|254|255)$/; + export const SUBNET_MASK_PATTERN: RegExp = /^(128|192|224|240|248|252|254|255).0.0.0|255.(0|128|192|224|240|248|252|254|255).0.0|255.255.(0|128|192|224|240|248|252|254|255).0|255.255.255.(0|128|192|224|240|248|252|254|255)$/; export const IPV4_PATTERN: RegExp = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; export const IPV6_PATTERN: RegExp = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|:((:[0-9a-fA-F]{1,4}){1,7})|::|([0-9a-fA-F]{1,4}:){1}(:[0-9a-fA-F]{1,4}){1,6}|([0-9a-fA-F]{1,4}:){2}(:[0-9a-fA-F]{1,4}){1,5}|([0-9a-fA-F]{1,4}:){3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){6}:[0-9a-fA-F]{1,4})$/; export const HOSTNAME_PATTERN: RegExp = /^([A-Za-z0-9][A-Za-z0-9-]*\.)*[A-Za-z][A-Za-z0-9-]*\.?$/; @@ -37,7 +37,7 @@ export namespace InetUtils { * @returns true if the value is a valid subnet mask */ export function isSubnetMask(value: string): boolean { - return SUBNET_MASK_PATTERN.test(value); + return value != null && SUBNET_MASK_PATTERN.test(value); } /** @@ -73,13 +73,13 @@ export namespace InetUtils { * @returns the detected network address type */ export function isNetworkAddress(value: string): IpType { - if (value === null || value.length == 0) {return IpType.None;} + if (value === null || value.length == 0) { return IpType.None; } const parts: string[] = value.split("/"); - if (parts.length != 2) {return IpType.None;} + if (parts.length != 2) { return IpType.None; } const cidrNum: number = Number.parseInt(parts[1], 10); - if (Number.isNaN(cidrNum)) {return IpType.None;} + if (Number.isNaN(cidrNum)) { return IpType.None; } const ipType = isIP(parts[0]); if (ipType === IpType.IPv4 && isValidIPv4Cidr(cidrNum)) { From 2e05a12716ba5a21234582917c48e650f17ff9b9 Mon Sep 17 00:00:00 2001 From: da Kai Date: Wed, 22 Apr 2026 21:33:44 +0200 Subject: [PATCH 10/11] quickfix --- ui/src/app/shared/utils/inet/inet.utils.spec.ts | 1 + ui/src/app/shared/utils/inet/inet.utils.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/app/shared/utils/inet/inet.utils.spec.ts b/ui/src/app/shared/utils/inet/inet.utils.spec.ts index 20698d69496..c38779d07a9 100644 --- a/ui/src/app/shared/utils/inet/inet.utils.spec.ts +++ b/ui/src/app/shared/utils/inet/inet.utils.spec.ts @@ -17,6 +17,7 @@ describe("InetUtils", () => { expect(InetUtils.isSubnetMask("255.0.255.0")).toBeFalse(); expect(InetUtils.isSubnetMask("255.192.255.192")).toBeFalse(); expect(InetUtils.isSubnetMask("0.0.0.0")).toBeFalse(); + expect(InetUtils.isSubnetMask("255:255:255:255")).toBeFalse(); expect(InetUtils.isSubnetMask(null)).toBeFalse(); }); diff --git a/ui/src/app/shared/utils/inet/inet.utils.ts b/ui/src/app/shared/utils/inet/inet.utils.ts index 25b49bd872a..615524a9cd3 100644 --- a/ui/src/app/shared/utils/inet/inet.utils.ts +++ b/ui/src/app/shared/utils/inet/inet.utils.ts @@ -5,7 +5,7 @@ export namespace InetUtils { IPv6 = 6, } - export const SUBNET_MASK_PATTERN: RegExp = /^(128|192|224|240|248|252|254|255).0.0.0|255.(0|128|192|224|240|248|252|254|255).0.0|255.255.(0|128|192|224|240|248|252|254|255).0|255.255.255.(0|128|192|224|240|248|252|254|255)$/; + export const SUBNET_MASK_PATTERN: RegExp = /^(128|192|224|240|248|252|254|255)\.0\.0\.0|255\.(0|128|192|224|240|248|252|254|255)\.0\.0|255\.255\.(0|128|192|224|240|248|252|254|255)\.0|255\.255\.255\.(0|128|192|224|240|248|252|254|255)$/; export const IPV4_PATTERN: RegExp = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; export const IPV6_PATTERN: RegExp = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|:((:[0-9a-fA-F]{1,4}){1,7})|::|([0-9a-fA-F]{1,4}:){1}(:[0-9a-fA-F]{1,4}){1,6}|([0-9a-fA-F]{1,4}:){2}(:[0-9a-fA-F]{1,4}){1,5}|([0-9a-fA-F]{1,4}:){3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){6}:[0-9a-fA-F]{1,4})$/; export const HOSTNAME_PATTERN: RegExp = /^([A-Za-z0-9][A-Za-z0-9-]*\.)*[A-Za-z][A-Za-z0-9-]*\.?$/; From bfbe25a3ac9804207a90d5c1772a854b2110696a Mon Sep 17 00:00:00 2001 From: da Kai Date: Wed, 22 Apr 2026 22:32:29 +0200 Subject: [PATCH 11/11] quickfix --- ui/src/app/shared/utils/inet/inet.utils.spec.ts | 2 ++ ui/src/app/shared/utils/inet/inet.utils.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/app/shared/utils/inet/inet.utils.spec.ts b/ui/src/app/shared/utils/inet/inet.utils.spec.ts index c38779d07a9..4a23e9651fb 100644 --- a/ui/src/app/shared/utils/inet/inet.utils.spec.ts +++ b/ui/src/app/shared/utils/inet/inet.utils.spec.ts @@ -14,6 +14,8 @@ describe("InetUtils", () => { expect(InetUtils.isSubnetMask("128.0.0.0")).toBeTrue(); expect(InetUtils.isSubnetMask("255.255.255.200")).toBeFalse(); + expect(InetUtils.isSubnetMask("255.255.255.255.255")).toBeFalse(); + expect(InetUtils.isSubnetMask("255.255.255")).toBeFalse(); expect(InetUtils.isSubnetMask("255.0.255.0")).toBeFalse(); expect(InetUtils.isSubnetMask("255.192.255.192")).toBeFalse(); expect(InetUtils.isSubnetMask("0.0.0.0")).toBeFalse(); diff --git a/ui/src/app/shared/utils/inet/inet.utils.ts b/ui/src/app/shared/utils/inet/inet.utils.ts index 615524a9cd3..ea8635fe562 100644 --- a/ui/src/app/shared/utils/inet/inet.utils.ts +++ b/ui/src/app/shared/utils/inet/inet.utils.ts @@ -5,7 +5,7 @@ export namespace InetUtils { IPv6 = 6, } - export const SUBNET_MASK_PATTERN: RegExp = /^(128|192|224|240|248|252|254|255)\.0\.0\.0|255\.(0|128|192|224|240|248|252|254|255)\.0\.0|255\.255\.(0|128|192|224|240|248|252|254|255)\.0|255\.255\.255\.(0|128|192|224|240|248|252|254|255)$/; + export const SUBNET_MASK_PATTERN: RegExp = /^(128|192|224|240|248|252|254|255)\.0\.0\.0$|^255\.(0|128|192|224|240|248|252|254|255)\.0\.0$|^255\.255\.(0|128|192|224|240|248|252|254|255)\.0$|^255\.255\.255\.(0|128|192|224|240|248|252|254|255)$/; export const IPV4_PATTERN: RegExp = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; export const IPV6_PATTERN: RegExp = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|:((:[0-9a-fA-F]{1,4}){1,7})|::|([0-9a-fA-F]{1,4}:){1}(:[0-9a-fA-F]{1,4}){1,6}|([0-9a-fA-F]{1,4}:){2}(:[0-9a-fA-F]{1,4}){1,5}|([0-9a-fA-F]{1,4}:){3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){6}:[0-9a-fA-F]{1,4})$/; export const HOSTNAME_PATTERN: RegExp = /^([A-Za-z0-9][A-Za-z0-9-]*\.)*[A-Za-z][A-Za-z0-9-]*\.?$/;