LCOV - code coverage report
Current view: top level - pkg/networkmanager - utils.js Coverage Total Hit
Test: cockpit Lines: 14.2 % 268 38
Test Date: 2026-06-16 14:09:37

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2016 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6            2 : import cockpit from "cockpit";
       7            2 : import * as ipaddr from "ipaddr.js";
       8              : 
       9            2 : const _ = cockpit.gettext;
      10              : 
      11              : /* NetworkManager specific data conversions and utility functions.
      12              :  */
      13              : 
      14            1 : let byteorder;
      15              : 
      16            1 : export function set_byteorder(bo) {
      17            1 :     byteorder = bo;
      18            1 : }
      19              : 
      20            1 : export function ip_prefix_to_text(num) {
      21            1 :     return num.toString();
      22            1 : }
      23              : 
      24            0 : export function ip_prefix_from_text(text) {
      25            0 :     if (/^[0-9]+$/.test(text.trim()))
      26            0 :         return parseInt(text, 10);
      27              : 
      28            0 :     throw cockpit.format(_("Invalid prefix $0"), text);
      29            0 : }
      30              : 
      31            0 : export function ip_metric_to_text(num) {
      32            0 :     return num.toString();
      33            0 : }
      34              : 
      35            0 : export function ip_metric_from_text(text) {
      36            0 :     if (text === "")
      37            0 :         return 0;
      38              : 
      39            0 :     if (/^[0-9]+$/.test(text.trim()))
      40            0 :         return parseInt(text, 10);
      41              : 
      42            0 :     throw cockpit.format(_("Invalid metric $0"), text);
      43            0 : }
      44              : 
      45            0 : export function ip_network_address(address, prefix_len) {
      46            0 :     const addrCIDR = address.toString() + "/" + prefix_len;
      47            0 :     const ip_obj = address.kind() === "ipv4" ? ipaddr.IPv4 : ipaddr.IPv6;
      48              : 
      49            0 :     try {
      50            0 :         return ip_obj.networkAddressFromCIDR(addrCIDR).toString();
      51            0 :     } catch (_e) {
      52            0 :         return null;
      53            0 :     }
      54            0 : }
      55              : 
      56            0 : export function ip_first_usable_address(address, prefix) {
      57            0 :     try {
      58            0 :         const addr_cidr = address.toString() + "/" + prefix;
      59              : 
      60            0 :         if (address.kind() === "ipv4") {
      61            0 :             const netAddr = ipaddr.IPv4.networkAddressFromCIDR(addr_cidr);
      62            0 :             netAddr.octets[3] += (prefix < 31) ? 1 : 0;
      63            0 :             return netAddr.toString();
      64            0 :         } else {
      65            0 :             const netAddr = ipaddr.IPv6.networkAddressFromCIDR(addr_cidr);
      66            0 :             netAddr.parts[7] += (prefix < 127) ? 1 : 0;
      67            0 :             return netAddr.toString();
      68            0 :         }
      69            0 :     } catch (_e) {
      70            0 :         return null;
      71            0 :     }
      72            0 : }
      73              : 
      74            0 : function toDec(n) {
      75            0 :     return n.toString(10);
      76            0 : }
      77              : 
      78            0 : function bytes_from_nm32(num) {
      79            0 :     const bytes = [];
      80            0 :     if (byteorder == "be") {
      81            0 :         for (let i = 3; i >= 0; i--) {
      82            0 :             bytes[i] = num & 0xFF;
      83            0 :             num = num >>> 8;
      84            0 :         }
      85            0 :     } else if (byteorder == "le") {
      86            0 :         for (let i = 0; i < 4; i++) {
      87            0 :             bytes[i] = num & 0xFF;
      88            0 :             num = num >>> 8;
      89            0 :         }
      90            0 :     } else {
      91            0 :         throw new Error("byteorder is unset or has invalid value " + JSON.stringify(byteorder));
      92            0 :     }
      93            0 :     return bytes;
      94            0 : }
      95              : 
      96            0 : export function validate_ipv4(address) {
      97              :     // explicitly require all 4 octets
      98              :     // NM does not support any IPv4 short format
      99            0 :     return ipaddr.IPv4.isValidFourPartDecimal(address);
     100            0 : }
     101              : 
     102            0 : export function validate_ipv6(address) {
     103            0 :     return ipaddr.IPv6.isValid(address);
     104            0 : }
     105              : 
     106            0 : export function validate_ip(address) {
     107            0 :     return validate_ipv4(address) || validate_ipv6(address);
     108            0 : }
     109              : 
     110            0 : export function ip4_to_text(num, zero_is_empty) {
     111            0 :     if (num === 0 && zero_is_empty)
     112            0 :         return "";
     113            0 :     return bytes_from_nm32(num).map(toDec)
     114            0 :             .join('.');
     115            0 : }
     116              : 
     117            0 : export function ip4_from_text(text, empty_is_zero) {
     118            0 :     function invalid() {
     119            0 :         throw cockpit.format(_("Invalid address $0"), text);
     120            0 :     }
     121              : 
     122            0 :     if (text === "" && empty_is_zero)
     123            0 :         return 0;
     124              : 
     125            0 :     const parts = text.split('.');
     126            0 :     if (parts.length != 4)
     127            0 :         invalid();
     128              : 
     129            0 :     const bytes = parts.map(function(s) {
     130            0 :         if (/^[0-9]+$/.test(s.trim()))
     131            0 :             return parseInt(s, 10);
     132              :         else
     133            0 :             return invalid();
     134            0 :     });
     135              : 
     136            0 :     let num = 0;
     137            0 :     function shift(b) {
     138            0 :         if (isNaN(b) || b < 0 || b > 0xFF)
     139            0 :             invalid();
     140            0 :         num = 0x100 * num + b;
     141            0 :     }
     142              : 
     143            0 :     if (byteorder == "be") {
     144            0 :         for (let i = 0; i < 4; i++) {
     145            0 :             shift(bytes[i]);
     146            0 :         }
     147            0 :     } else if (byteorder == "le") {
     148            0 :         for (let i = 3; i >= 0; i--) {
     149            0 :             shift(bytes[i]);
     150            0 :         }
     151            0 :     } else {
     152            0 :         throw new Error("byteorder is unset or has invalid value " + JSON.stringify(byteorder));
     153            0 :     }
     154              : 
     155            0 :     return num;
     156            0 : }
     157              : 
     158            0 : export function ip4_prefix_from_text(prefix_mask) {
     159            0 :     const trimmed_mask = prefix_mask.trim();
     160            0 :     if (/^[0-9]+$/.test(trimmed_mask)) {
     161            0 :         return parseInt(prefix_mask, 10);
     162            0 :     }
     163              : 
     164              :     // make sure that mask has 4 octets format
     165            0 :     if (validate_ipv4(trimmed_mask)) {
     166            0 :         const prefix = ipaddr.IPv4.parse(trimmed_mask).prefixLengthFromSubnetMask();
     167            0 :         if (prefix !== null) {
     168            0 :             return prefix;
     169            0 :         }
     170            0 :     }
     171              : 
     172            0 :     throw cockpit.format(_("Invalid prefix or netmask $0"), prefix_mask);
     173            0 : }
     174              : 
     175              : // Shorten IPv6 address according to RFC 5952
     176              : // https://datatracker.ietf.org/doc/html/rfc5952#section-4
     177              : //
     178              : // NetworkManager already handles dropping of leadin zeros within a single 16 bit field
     179              : // but does not replace the longest consecutive zeros fields with "::".
     180            0 : function ip6_shorten(ip6_addr) {
     181            0 :     function find_longest_zero(match) {
     182            0 :         let idx = -1;
     183            0 :         let length = 0;
     184              : 
     185            0 :         match.forEach((item, i) => {
     186            0 :             const count_zero = item[0].replaceAll(':', '').length;
     187            0 :             if (count_zero > length) {
     188            0 :                 idx = i;
     189            0 :                 length = count_zero;
     190            0 :             }
     191            0 :         });
     192              : 
     193            0 :         return idx;
     194            0 :     }
     195              : 
     196            0 :     const REGEX_MATCH_CONSECUTIVE_ZEROS = /\b:?(?:0:?){2,}/g;
     197            0 :     const match = [...ip6_addr.matchAll(REGEX_MATCH_CONSECUTIVE_ZEROS)];
     198              : 
     199              :     // nothing to shorten
     200            0 :     if (match.length === 0) {
     201            0 :         return ip6_addr;
     202            0 :     }
     203              : 
     204            0 :     const longest_idx = find_longest_zero(match);
     205              :     // replace first (leftmost) match
     206            0 :     const short_addr = ip6_addr.replace(match[longest_idx], "::");
     207              : 
     208            0 :     return short_addr;
     209            0 : }
     210              : 
     211            0 : export function ip6_to_text(data, zero_is_empty) {
     212            0 :     const parts = [];
     213            0 :     const bytes = cockpit.base64_decode(data);
     214            0 :     for (let i = 0; i < 8; i++)
     215            0 :         parts[i] = ((bytes[2 * i] << 8) + bytes[2 * i + 1]).toString(16);
     216            0 :     const result = parts.join(':');
     217            0 :     if (result == "0:0:0:0:0:0:0:0" && zero_is_empty)
     218            0 :         return "";
     219            0 :     return ip6_shorten(result);
     220            0 : }
     221              : 
     222            0 : export function ip6_from_text(text, empty_is_zero) {
     223            0 :     function invalid() {
     224            0 :         throw cockpit.format(_("Invalid address $0"), text);
     225            0 :     }
     226              : 
     227            0 :     if (text === "" && empty_is_zero)
     228            0 :         return cockpit.base64_encode([0, 0, 0, 0, 0, 0, 0, 0,
     229            0 :             0, 0, 0, 0, 0, 0, 0, 0,
     230            0 :         ]);
     231              : 
     232            0 :     const parts = text.split(':');
     233            0 :     if (parts.length < 1 || parts.length > 8)
     234            0 :         invalid();
     235              : 
     236            0 :     if (parts[0] === "")
     237            0 :         parts[0] = "0";
     238            0 :     if (parts[parts.length - 1] === "")
     239            0 :         parts[parts.length - 1] = "0";
     240              : 
     241            0 :     const bytes = [];
     242            0 :     let empty_seen = false;
     243            0 :     let j = 0;
     244            0 :     for (let i = 0; i < parts.length; i++, j++) {
     245            0 :         if (parts[i] === "") {
     246            0 :             if (empty_seen)
     247            0 :                 invalid();
     248            0 :             empty_seen = true;
     249            0 :             while (j < i + (8 - parts.length)) {
     250            0 :                 bytes[2 * j] = bytes[2 * j + 1] = 0;
     251            0 :                 j++;
     252            0 :             }
     253            0 :         } else {
     254            0 :             if (!/^[0-9a-fA-F]+$/.test(parts[i].trim()))
     255            0 :                 invalid();
     256            0 :             const n = parseInt(parts[i], 16);
     257            0 :             if (isNaN(n) || n < 0 || n > 0xFFFF)
     258            0 :                 invalid();
     259            0 :             bytes[2 * j] = n >> 8;
     260            0 :             bytes[2 * j + 1] = n & 0xFF;
     261            0 :         }
     262            0 :     }
     263            0 :     if (j != 8)
     264            0 :         invalid();
     265              : 
     266            0 :     return cockpit.base64_encode(bytes);
     267            0 : }
     268              : 
     269              : // SSID comes as a base64-encoded string from D-Bus (signature 'ay')
     270            0 : export const ssid_from_nm = bytes => new TextDecoder().decode(
     271            0 :     new Uint8Array(cockpit.base64_decode(bytes ?? []))
     272            0 : );
     273              : 
     274            0 : export const ssid_to_nm = ssid => cockpit.base64_encode(new TextEncoder().encode(ssid));
     275              : 
     276            2 : export function list_interfaces() {
     277            2 :     return new Promise((resolve, reject) => {
     278            2 :         const client = cockpit.dbus("org.freedesktop.NetworkManager");
     279            2 :         client.call('/org/freedesktop/NetworkManager',
     280            2 :                     'org.freedesktop.NetworkManager',
     281            2 :                     'GetAllDevices', [])
     282            2 :                 .then(reply => {
     283            2 :                     Promise.all(reply[0].map(device => {
     284            2 :                         return Promise.all([
     285            2 :                             client.call(device,
     286            2 :                                         'org.freedesktop.DBus.Properties',
     287            2 :                                         'Get', ['org.freedesktop.NetworkManager.Device', 'Interface'])
     288            2 :                                     .then(reply => reply[0]),
     289            2 :                             client.call(device,
     290            2 :                                         'org.freedesktop.DBus.Properties',
     291            2 :                                         'Get', ['org.freedesktop.NetworkManager.Device', 'Capabilities'])
     292            2 :                                     .then(reply => reply[0])
     293            2 :                         ]);
     294            2 :                     }))
     295            2 :                             .then(interfaces => {
     296            2 :                                 client.close();
     297            2 :                                 resolve(interfaces.map(i => {
     298            2 :                                     return { device: i[0].v, capabilities: i[1].v };
     299            2 :                                 }));
     300            2 :                             })
     301            0 :                             .catch(e => console.warn(JSON.stringify(e)));
     302            2 :                 })
     303            0 :                 .catch(e => console.warn(JSON.stringify(e)));
     304            2 :     });
     305            2 : }
     306              : 
     307              : // Check if we are working with non-persistent multi-connection,
     308              : // namely the `id=Wired Connection` generated by networkmanager.
     309              : //
     310              : // When editing this connection, we need to create a new connection
     311              : // on top of this, remove the multiconnection settings and assign it directly
     312              : // to interface. This is done for Anaconda in order to avoid creating unexpected
     313              : // multiconnections when user modifies the connection during installation.
     314            0 : export function isNonPersistentMultiCon(connection) {
     315            0 :     const settings = connection?.Settings;
     316            0 :     if (settings &&
     317            0 :         settings.connection.interface_name === undefined &&
     318            0 :         settings.connection.id === "Wired Connection" &&
     319            0 :         connection.Unsaved === true && settings.connection.type &&
     320            0 :         settings.connection.multi_connect === 3 &&
     321            0 :         settings.connection.type === "802-3-ethernet") {
     322            0 :         return true;
     323            0 :     }
     324              : 
     325            0 :     return false;
     326            0 : }
     327              : 
     328            0 : export function createNewConnSettings(settings, iface_name) {
     329            0 :     const newSettings = {
     330            0 :         ...settings,
     331            0 :         connection: {
     332            0 :             ...settings.connection,
     333            0 :             id: iface_name,
     334            0 :             interface_name: iface_name,
     335              :             // increase the current priority by 1, maximum priority is 999
     336            0 :             autoconnect_priority: Math.min(settings.connection.autoconnect_priority + 1, 999),
     337            0 :         }
     338            0 :     };
     339            0 :     delete newSettings.connection.uuid;
     340            0 :     delete newSettings.connection.multi_connect;
     341              : 
     342            0 :     return newSettings;
     343            0 : }
     344              : 
     345            0 : export function debug() {
     346            0 :     if (window.debugging == "all" || window.debugging?.includes("networkmanager")) // not-covered: debugging
     347            0 :         console.debug("networkmanager:", ...arguments); // not-covered: debugging
     348            0 : }
        

Generated by: LCOV version 2.0-1