diff options
Diffstat (limited to 'static/systemd-network.js')
| -rw-r--r-- | static/systemd-network.js | 590 |
1 files changed, 277 insertions, 313 deletions
diff --git a/static/systemd-network.js b/static/systemd-network.js index 8c66a6d..1a33f0e 100644 --- a/static/systemd-network.js +++ b/static/systemd-network.js @@ -7,56 +7,71 @@ */ /** - * MAC Address type - * @class MACAddress + * Base field type with standardized interface + * @class BaseField */ -class MACAddress { +class BaseField { /** - * @param {string} value - MAC address value - * @param {string} description - Description + * @param {*} value - Field value + * @param {string} type - Field type + * @param {string} description - Field description + * @param {Object} options - Additional options (pattern, enum, etc.) */ - constructor(value = '', description = 'Hardware address') { + constructor(value = null, type = 'string', description = '', options = {}) { this.value = value; - this.type = 'mac-address'; + this.type = type; this.description = description; - this.pattern = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/; + this.options = options; } /** - * Validate MAC address format + * Validate field value * @returns {boolean} */ validate() { - return this.pattern.test(this.value); + if (this.value === null || this.value === undefined) return true; + + if (this.options.pattern && typeof this.value === 'string') { + return this.options.pattern.test(this.value); + } + + if (this.options.enum && this.options.enum.length > 0) { + return this.options.enum.includes(this.value); + } + + return true; } + /** + * Convert to string representation + * @returns {string} + */ toString() { - return this.value; + return this.value !== null ? String(this.value) : ''; } } /** - * IPv4 Address type - * @class IPv4Address + * MAC Address type + * @class MACAddress */ -class IPv4Address { - /** - * @param {string} value - IPv4 address - * @param {string} description - Description - */ - constructor(value = '', description = 'IPv4 address') { - this.value = value; - this.type = 'ipv4-address'; - this.description = description; - this.pattern = /^(\d{1,3}\.){3}\d{1,3}$/; - } - - validate() { - return this.pattern.test(this.value); +class MACAddress extends BaseField { + constructor(value = null) { + super(value, 'mac-address', 'Hardware address (MAC)', { + pattern: /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/ + }); } +} - toString() { - return this.value; +/** + * IPv4 Address type + * @class IPv4Address + */ +class IPv4Address extends BaseField { + constructor(value = null) { + super(value, 'ipv4-address', 'IPv4 address', { + pattern: /^(\d{1,3}\.){3}\d{1,3}$/ + }); } } @@ -64,24 +79,47 @@ class IPv4Address { * IPv6 Address type * @class IPv6Address */ -class IPv6Address { - /** - * @param {string} value - IPv6 address - * @param {string} description - Description - */ - constructor(value = '', description = 'IPv6 address') { - this.value = value; - this.type = 'ipv6-address'; - this.description = description; - this.pattern = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/; +class IPv6Address extends BaseField { + constructor(value = null) { + super(value, 'ipv6-address', 'IPv6 address', { + pattern: /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/ + }); } +} - validate() { - return this.pattern.test(this.value); +/** + * Boolean type with yes/no values + * @class BooleanYesNo + */ +class BooleanYesNo extends BaseField { + constructor(value = null) { + super(value, 'boolean', 'Boolean (yes/no)', { + enum: ['yes', 'no'] + }); } +} - toString() { - return this.value; +/** + * Port type + * @class Port + */ +class Port extends BaseField { + constructor(value = null) { + super(value, 'port', 'Network port (1-65535)', { + pattern: /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/ + }); + } +} + +/** + * MTU type + * @class MTU + */ +class MTU extends BaseField { + constructor(value = null) { + super(value, 'mtu', 'Maximum Transmission Unit', { + pattern: /^[1-9][0-9]*$/ + }); } } @@ -91,28 +129,17 @@ class IPv6Address { */ class MatchSection { constructor() { - /** @type {MACAddress[]} */ - this.MACAddress = []; - /** @type {string[]} */ - this.OriginalName = []; - /** @type {string[]} */ - this.Path = []; - /** @type {string[]} */ - this.Driver = []; - /** @type {string[]} */ - this.Type = []; - /** @type {string[]} */ - this.Name = []; - /** @type {string} */ - this.Property = ''; - /** @type {string} */ - this.Host = ''; - /** @type {string} */ - this.Virtualization = ''; - /** @type {string} */ - this.KernelCommandLine = ''; - /** @type {string} */ - this.Architecture = ''; + this.MACAddress = new BaseField(null, 'mac-addresses', 'Space-separated MAC addresses'); + this.OriginalName = new BaseField(null, 'strings', 'Original interface names'); + this.Path = new BaseField(null, 'strings', 'Device path patterns'); + this.Driver = new BaseField(null, 'strings', 'Driver names'); + this.Type = new BaseField(null, 'strings', 'Interface types (ether, wifi, etc.)'); + this.Name = new BaseField(null, 'strings', 'Interface names'); + this.Property = new BaseField(null, 'string', 'Device property'); + this.Host = new BaseField(null, 'string', 'Host name'); + this.Virtualization = new BaseField(null, 'string', 'Virtualization detection'); + this.KernelCommandLine = new BaseField(null, 'string', 'Kernel command line'); + this.Architecture = new BaseField(null, 'string', 'System architecture'); } } @@ -122,26 +149,22 @@ class MatchSection { */ class LinkSection { constructor() { - /** @type {string} */ - this.MACAddress = ''; - /** @type {string} */ - this.MTUBytes = ''; - /** @type {number} */ - this.BitsPerSecond = 0; - /** @type {string} */ - this.Duplex = ''; - /** @type {string} */ - this.AutoNegotiation = ''; - /** @type {string} */ - this.WakeOnLan = ''; - /** @type {string} */ - this.Port = ''; - /** @type {string} */ - this.Advertise = ''; - /** @type {string} */ - this.RxFlowControl = ''; - /** @type {string} */ - this.TxFlowControl = ''; + this.MACAddress = new MACAddress(); + this.MTUBytes = new MTU(); + this.BitsPerSecond = new BaseField(null, 'number', 'Link speed in bits per second'); + this.Duplex = new BaseField(null, 'string', 'Duplex mode', { + enum: ['half', 'full'] + }); + this.AutoNegotiation = new BooleanYesNo(); + this.WakeOnLan = new BaseField(null, 'string', 'Wake-on-LAN', { + enum: ['phy', 'unicast', 'broadcast', 'arp', 'magic', ''] + }); + this.Port = new BaseField(null, 'string', 'Port type', { + enum: ['tp', 'aui', 'bnc', 'mii', 'fibre', ''] + }); + this.Advertise = new BaseField(null, 'strings', 'Advertised features'); + this.RxFlowControl = new BooleanYesNo(); + this.TxFlowControl = new BooleanYesNo(); } } @@ -151,36 +174,33 @@ class LinkSection { */ class NetworkSection { constructor() { - /** @type {string} */ - this.Description = ''; - /** @type {string[]} */ - this.DHCP = []; // 'yes', 'no', 'ipv4', 'ipv6' - /** @type {boolean} */ - this.DHCPServer = false; - /** @type {string[]} */ - this.DNS = []; - /** @type {string[]} */ - this.NTP = []; - /** @type {string[]} */ - this.IPForward = []; // 'yes', 'no', 'ipv4', 'ipv6' - /** @type {string} */ - this.IPv6PrivacyExtensions = ''; // 'yes', 'no', 'prefer-public' - /** @type {string} */ - this.IPv6AcceptRA = ''; // 'yes', 'no' - /** @type {string} */ - this.LLMNR = ''; // 'yes', 'no', 'resolve' - /** @type {string} */ - this.MulticastDNS = ''; // 'yes', 'no', 'resolve' - /** @type {string} */ - this.DNSSEC = ''; // 'yes', 'no', 'allow-downgrade' - /** @type {string[]} */ - this.Domains = []; - /** @type {string} */ - this.ConfigureWithoutCarrier = ''; // 'yes', 'no' - /** @type {string} */ - this.IgnoreCarrierLoss = ''; // 'yes', 'no' - /** @type {number} */ - this.KeepConfiguration = 0; // seconds + this.Description = new BaseField(null, 'string', 'Interface description'); + this.DHCP = new BaseField(null, 'dhcp-mode', 'DHCP client', { + enum: ['yes', 'no', 'ipv4', 'ipv6'] + }); + this.DHCPServer = new BooleanYesNo(); + this.DNS = new BaseField(null, 'ip-addresses', 'DNS servers'); + this.NTP = new BaseField(null, 'ip-addresses', 'NTP servers'); + this.IPForward = new BaseField(null, 'ip-forward', 'IP forwarding', { + enum: ['yes', 'no', 'ipv4', 'ipv6'] + }); + this.IPv6PrivacyExtensions = new BaseField(null, 'privacy-extensions', 'IPv6 privacy extensions', { + enum: ['yes', 'no', 'prefer-public'] + }); + this.IPv6AcceptRA = new BooleanYesNo(); + this.LLMNR = new BaseField(null, 'llmnr', 'LLMNR support', { + enum: ['yes', 'no', 'resolve'] + }); + this.MulticastDNS = new BaseField(null, 'mdns', 'Multicast DNS', { + enum: ['yes', 'no', 'resolve'] + }); + this.DNSSEC = new BaseField(null, 'dnssec', 'DNSSEC support', { + enum: ['yes', 'no', 'allow-downgrade'] + }); + this.Domains = new BaseField(null, 'strings', 'DNS search domains'); + this.ConfigureWithoutCarrier = new BooleanYesNo(); + this.IgnoreCarrierLoss = new BooleanYesNo(); + this.KeepConfiguration = new BaseField(null, 'number', 'Keep configuration time in seconds'); } } @@ -190,20 +210,17 @@ class NetworkSection { */ class DHCPSection { constructor() { - /** @type {string} */ - this.UseDNS = ''; // 'yes', 'no' - /** @type {string} */ - this.UseNTP = ''; // 'yes', 'no' - /** @type {string} */ - this.UseMTU = ''; // 'yes', 'no' - /** @type {string} */ - this.UseHostname = ''; // 'yes', 'no' - /** @type {string} */ - this.UseDomains = ''; // 'yes', 'no', 'route' - /** @type {string} */ - this.ClientIdentifier = ''; // 'mac', 'duid' - /** @type {string} */ - this.RouteMetric = ''; + this.UseDNS = new BooleanYesNo(); + this.UseNTP = new BooleanYesNo(); + this.UseMTU = new BooleanYesNo(); + this.UseHostname = new BooleanYesNo(); + this.UseDomains = new BaseField(null, 'use-domains', 'Use domains from DHCP', { + enum: ['yes', 'no', 'route'] + }); + this.ClientIdentifier = new BaseField(null, 'client-identifier', 'DHCP client identifier', { + enum: ['mac', 'duid'] + }); + this.RouteMetric = new BaseField(null, 'number', 'Route metric for DHCP routes'); } } @@ -213,18 +230,12 @@ class DHCPSection { */ class AddressSection { constructor() { - /** @type {string} */ - this.Address = ''; // IP address with prefix - /** @type {string} */ - this.Peer = ''; // Peer address - /** @type {string} */ - this.Broadcast = ''; // Broadcast address - /** @type {string} */ - this.Label = ''; // Address label - /** @type {number} */ - this.Scope = 0; // Address scope - /** @type {string} */ - this.Flags = ''; // Address flags + this.Address = new BaseField(null, 'ip-prefix', 'IP address with prefix'); + this.Peer = new BaseField(null, 'ip-address', 'Peer address'); + this.Broadcast = new BaseField(null, 'ip-address', 'Broadcast address'); + this.Label = new BaseField(null, 'string', 'Address label'); + this.Scope = new BaseField(null, 'number', 'Address scope'); + this.Flags = new BaseField(null, 'strings', 'Address flags'); } } @@ -234,22 +245,18 @@ class AddressSection { */ class RouteSection { constructor() { - /** @type {string} */ - this.Gateway = ''; // Gateway address - /** @type {string} */ - this.GatewayOnLink = ''; // 'yes', 'no' - /** @type {string} */ - this.Destination = ''; // Destination prefix - /** @type {string} */ - this.Source = ''; // Source address - /** @type {string} */ - this.PreferredSource = ''; // Preferred source address - /** @type {number} */ - this.Metric = 1024; // Route metric - /** @type {string} */ - this.Scope = ''; // 'global', 'link', 'host' - /** @type {string} */ - this.Type = ''; // 'unicast', 'local', 'broadcast', etc. + this.Gateway = new BaseField(null, 'ip-address', 'Gateway address'); + this.GatewayOnLink = new BooleanYesNo(); + this.Destination = new BaseField(null, 'ip-prefix', 'Destination prefix'); + this.Source = new BaseField(null, 'ip-address', 'Source address'); + this.PreferredSource = new BaseField(null, 'ip-address', 'Preferred source address'); + this.Metric = new BaseField(null, 'number', 'Route metric'); + this.Scope = new BaseField(null, 'route-scope', 'Route scope', { + enum: ['global', 'link', 'host'] + }); + this.Type = new BaseField(null, 'route-type', 'Route type', { + enum: ['unicast', 'local', 'broadcast', 'anycast', 'multicast', 'blackhole', 'unreachable', 'prohibit'] + }); } } @@ -259,21 +266,64 @@ class RouteSection { */ class NetworkConfiguration { constructor() { - /** @type {MatchSection} */ this.Match = new MatchSection(); - /** @type {LinkSection} */ this.Link = new LinkSection(); - /** @type {NetworkSection} */ this.Network = new NetworkSection(); - /** @type {DHCPSection} */ this.DHCP = new DHCPSection(); - /** @type {AddressSection[]} */ this.Address = []; - /** @type {RouteSection[]} */ this.Route = []; } /** + * Get schema for structured editor + * @returns {Object} Schema definition + */ + getSchema() { + return { + Match: this._getSectionSchema(this.Match), + Link: this._getSectionSchema(this.Link), + Network: this._getSectionSchema(this.Network), + DHCP: this._getSectionSchema(this.DHCP), + Address: this._getArraySectionSchema(AddressSection, 'Address'), + Route: this._getArraySectionSchema(RouteSection, 'Route') + }; + } + + /** + * Get schema for a single section + * @private + * @param {Object} section - Section instance + * @returns {Object} Section schema + */ + _getSectionSchema(section) { + const schema = {}; + for (const [key, field] of Object.entries(section)) { + schema[key] = { + value: field.value, + type: field.type, + description: field.description, + options: field.options + }; + } + return schema; + } + + /** + * Get schema for array sections (Address, Route) + * @private + * @param {Class} SectionClass - Section class + * @param {string} sectionName - Section name + * @returns {Object} Array section schema + */ + _getArraySectionSchema(SectionClass, sectionName) { + const template = new SectionClass(); + return { + itemSchema: this._getSectionSchema(template), + items: this[sectionName].map(item => this._getSectionSchema(item)) + }; + } + + /** * Parse systemd network configuration from text * @param {string} configText - Configuration file content * @returns {NetworkConfiguration} @@ -282,6 +332,8 @@ class NetworkConfiguration { const config = new NetworkConfiguration(); const lines = configText.split('\n'); let currentSection = null; + let currentAddress = null; + let currentRoute = null; for (const line of lines) { const trimmed = line.trim(); @@ -293,6 +345,15 @@ class NetworkConfiguration { const sectionMatch = trimmed.match(/^\[(\w+)\]$/); if (sectionMatch) { currentSection = sectionMatch[1].toLowerCase(); + + // Start new array sections + if (currentSection === 'address') { + currentAddress = new AddressSection(); + config.Address.push(currentAddress); + } else if (currentSection === 'route') { + currentRoute = new RouteSection(); + config.Route.push(currentRoute); + } continue; } @@ -302,7 +363,7 @@ class NetworkConfiguration { const key = kvMatch[1]; const value = kvMatch[2]; - config._setValue(currentSection, key, value); + config._setValue(currentSection, key, value, currentAddress, currentRoute); } } @@ -315,8 +376,10 @@ class NetworkConfiguration { * @param {string} section - Section name * @param {string} key - Key name * @param {string} value - Value + * @param {AddressSection} currentAddress - Current address section + * @param {RouteSection} currentRoute - Current route section */ - _setValue(section, key, value) { + _setValue(section, key, value, currentAddress, currentRoute) { switch (section) { case 'match': this._setMatchValue(key, value); @@ -331,70 +394,52 @@ class NetworkConfiguration { this._setDHCPValue(key, value); break; case 'address': - // Handle multiple address sections - if (!this.Address.length) this.Address.push(new AddressSection()); - this._setAddressValue(this.Address[this.Address.length - 1], key, value); + if (currentAddress) { + this._setAddressValue(currentAddress, key, value); + } break; case 'route': - // Handle multiple route sections - if (!this.Route.length) this.Route.push(new RouteSection()); - this._setRouteValue(this.Route[this.Route.length - 1], key, value); + if (currentRoute) { + this._setRouteValue(currentRoute, key, value); + } break; } } _setMatchValue(key, value) { - const match = this.Match; - switch (key) { - case 'MACAddress': - match.MACAddress = value.split(' '); - break; - case 'Name': - match.Name = value.split(' '); - break; - case 'Driver': - match.Driver = value.split(' '); - break; - case 'Path': - match.Path = value.split(' '); - break; - case 'Type': - match.Type = value.split(' '); - break; - default: - match[key] = value; + if (this.Match[key] !== undefined) { + this.Match[key].value = value; } } _setLinkValue(key, value) { - this.Link[key] = value; + if (this.Link[key] !== undefined) { + this.Link[key].value = value; + } } _setNetworkValue(key, value) { - const network = this.Network; - switch (key) { - case 'DNS': - case 'NTP': - case 'Domains': - case 'DHCP': - case 'IPForward': - network[key] = value.split(' '); - break; - default: - network[key] = value; + if (this.Network[key] !== undefined) { + this.Network[key].value = value; } } _setDHCPValue(key, value) { - this.DHCP[key] = value; + if (this.DHCP[key] !== undefined) { + this.DHCP[key].value = value; + } } _setAddressValue(address, key, value) { - address[key] = value; + if (address[key] !== undefined) { + address[key].value = value; + } } _setRouteValue(route, key, value) { - route[key] = value; + if (route[key] !== undefined) { + route[key].value = value; + } } /** @@ -405,155 +450,74 @@ class NetworkConfiguration { const sections = []; // [Match] section - if (this._hasMatchValues()) { + if (this._hasSectionValues(this.Match)) { sections.push('[Match]'); - sections.push(...this._formatMatchSection()); + sections.push(...this._formatSection(this.Match)); } // [Link] section - if (this._hasLinkValues()) { + if (this._hasSectionValues(this.Link)) { sections.push('[Link]'); - sections.push(...this._formatLinkSection()); + sections.push(...this._formatSection(this.Link)); } // [Network] section - if (this._hasNetworkValues()) { + if (this._hasSectionValues(this.Network)) { sections.push('[Network]'); - sections.push(...this._formatNetworkSection()); + sections.push(...this._formatSection(this.Network)); } // [DHCP] section - if (this._hasDHCPValues()) { + if (this._hasSectionValues(this.DHCP)) { sections.push('[DHCP]'); - sections.push(...this._formatDHCPSection()); + sections.push(...this._formatSection(this.DHCP)); } // [Address] sections - this.Address.forEach((addr, index) => { - if (this._hasAddressValues(addr)) { + this.Address.forEach(addr => { + if (this._hasSectionValues(addr)) { sections.push('[Address]'); - sections.push(...this._formatAddressSection(addr)); + sections.push(...this._formatSection(addr)); } }); // [Route] sections - this.Route.forEach((route, index) => { - if (this._hasRouteValues(route)) { + this.Route.forEach(route => { + if (this._hasSectionValues(route)) { sections.push('[Route]'); - sections.push(...this._formatRouteSection(route)); + sections.push(...this._formatSection(route)); } }); return sections.join('\n') + '\n'; } - // Helper methods to check if sections have values - _hasMatchValues() { - return Object.values(this.Match).some(val => - val && (Array.isArray(val) ? val.length > 0 : val !== '') + _hasSectionValues(section) { + return Object.values(section).some(field => + field.value !== null && field.value !== undefined && field.value !== '' ); } - _hasLinkValues() { - return Object.values(this.Link).some(val => val && val !== ''); - } - - _hasNetworkValues() { - return Object.values(this.Network).some(val => - val && (Array.isArray(val) ? val.length > 0 : val !== '') - ); - } - - _hasDHCPValues() { - return Object.values(this.DHCP).some(val => val && val !== ''); - } - - _hasAddressValues(addr) { - return Object.values(addr).some(val => val && val !== ''); - } - - _hasRouteValues(route) { - return Object.values(route).some(val => val && val !== ''); - } - - // Formatting methods - _formatMatchSection() { + _formatSection(section) { const lines = []; - const match = this.Match; - - if (match.MACAddress.length) lines.push(`MACAddress=${match.MACAddress.join(' ')}`); - if (match.Name.length) lines.push(`Name=${match.Name.join(' ')}`); - if (match.Driver.length) lines.push(`Driver=${match.Driver.join(' ')}`); - if (match.Path.length) lines.push(`Path=${match.Path.join(' ')}`); - if (match.Type.length) lines.push(`Type=${match.Type.join(' ')}`); - if (match.Property) lines.push(`Property=${match.Property}`); - - return lines; - } - - _formatLinkSection() { - const lines = []; - const link = this.Link; - - Object.entries(link).forEach(([key, value]) => { - if (value && value !== '') lines.push(`${key}=${value}`); - }); - - return lines; - } - - _formatNetworkSection() { - const lines = []; - const network = this.Network; - - if (network.Description) lines.push(`Description=${network.Description}`); - if (network.DHCP.length) lines.push(`DHCP=${network.DHCP.join(' ')}`); - if (network.DNS.length) lines.push(`DNS=${network.DNS.join(' ')}`); - if (network.NTP.length) lines.push(`NTP=${network.NTP.join(' ')}`); - if (network.IPForward.length) lines.push(`IPForward=${network.IPForward.join(' ')}`); - if (network.IPv6PrivacyExtensions) lines.push(`IPv6PrivacyExtensions=${network.IPv6PrivacyExtensions}`); - if (network.LLMNR) lines.push(`LLMNR=${network.LLMNR}`); - - return lines; - } - - _formatDHCPSection() { - const lines = []; - const dhcp = this.DHCP; - - Object.entries(dhcp).forEach(([key, value]) => { - if (value && value !== '') lines.push(`${key}=${value}`); - }); - - return lines; - } - - _formatAddressSection(addr) { - const lines = []; - - Object.entries(addr).forEach(([key, value]) => { - if (value && value !== '') lines.push(`${key}=${value}`); - }); - - return lines; - } - - _formatRouteSection(route) { - const lines = []; - - Object.entries(route).forEach(([key, value]) => { - if (value && value !== '') lines.push(`${key}=${value}`); - }); - + for (const [key, field] of Object.entries(section)) { + if (field.value !== null && field.value !== undefined && field.value !== '') { + lines.push(`${key}=${field.toString()}`); + } + } return lines; } } // Export classes export { + BaseField, MACAddress, IPv4Address, IPv6Address, + BooleanYesNo, + Port, + MTU, MatchSection, LinkSection, NetworkSection, |
