LCOV - code coverage report
Current view: top level - pkg/networkmanager - utils.js Coverage Total Hit
Test: cockpit Lines: 46.3 % 268 124
Test Date: 2026-07-17 07:33:01

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2016 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6           44 : import cockpit from "cockpit";
       7           44 : import * as ipaddr from "ipaddr.js";
       8              : 
       9           44 : const _ = cockpit.gettext;
      10              : 
      11              : /* NetworkManager specific data conversions and utility functions.
      12              :  */
      13              : 
      14           37 : let byteorder;
      15              : 
      16           37 : export function set_byteorder(bo) {
      17           37 :     byteorder = bo;
      18           37 : }
      19              : 
      20           37 : export function ip_prefix_to_text(num) {
      21           37 :     return num.toString();
      22           37 : }
      23              : 
      24            4 : export function ip_prefix_from_text(text) {
      25            4 :     if (/^[0-9]+$/.test(text.trim()))
      26            4 :         return parseInt(text, 10);
      27              : 
      28            1 :     throw cockpit.format(_("Invalid prefix $0"), text);
      29            4 : }
      30              : 
      31            2 : export function ip_metric_to_text(num) {
      32            2 :     return num.toString();
      33            2 : }
      34              : 
      35            2 : export function ip_metric_from_text(text) {
      36            2 :     if (text === "")
      37            0 :         return 0;
      38              : 
      39            2 :     if (/^[0-9]+$/.test(text.trim()))
      40            2 :         return parseInt(text, 10);
      41              : 
      42            0 :     throw cockpit.format(_("Invalid metric $0"), text);
      43            2 : }
      44              : 
      45            7 : export function ip_network_address(address, prefix_len) {
      46            7 :     const addrCIDR = address.toString() + "/" + prefix_len;
      47            2 :     const ip_obj = address.kind() === "ipv4" ? ipaddr.IPv4 : ipaddr.IPv6;
      48              : 
      49            7 :     try {
      50            7 :         return ip_obj.networkAddressFromCIDR(addrCIDR).toString();
      51            0 :     } catch (_e) {
      52            0 :         return null;
      53            0 :     }
      54            7 : }
      55              : 
      56            7 : export function ip_first_usable_address(address, prefix) {
      57            7 :     try {
      58            7 :         const addr_cidr = address.toString() + "/" + prefix;
      59              : 
      60            7 :         if (address.kind() === "ipv4") {
      61            7 :             const netAddr = ipaddr.IPv4.networkAddressFromCIDR(addr_cidr);
      62            0 :             netAddr.octets[3] += (prefix < 31) ? 1 : 0;
      63            7 :             return netAddr.toString();
      64            2 :         } else {
      65            2 :             const netAddr = ipaddr.IPv6.networkAddressFromCIDR(addr_cidr);
      66            0 :             netAddr.parts[7] += (prefix < 127) ? 1 : 0;
      67            2 :             return netAddr.toString();
      68            2 :         }
      69            0 :     } catch (_e) {
      70            0 :         return null;
      71            0 :     }
      72            7 : }
      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           10 : export function validate_ipv4(address) {
      97              :     // explicitly require all 4 octets
      98              :     // NM does not support any IPv4 short format
      99           10 :     return ipaddr.IPv4.isValidFourPartDecimal(address);
     100           10 : }
     101              : 
     102            8 : export function validate_ipv6(address) {
     103            8 :     return ipaddr.IPv6.isValid(address);
     104            8 : }
     105              : 
     106           10 : export function validate_ip(address) {
     107            8 :     return validate_ipv4(address) || validate_ipv6(address);
     108           10 : }
     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           10 : export function ip4_prefix_from_text(prefix_mask) {
     159           10 :     const trimmed_mask = prefix_mask.trim();
     160           10 :     if (/^[0-9]+$/.test(trimmed_mask)) {
     161           10 :         return parseInt(prefix_mask, 10);
     162           10 :     }
     163              : 
     164              :     // make sure that mask has 4 octets format
     165            3 :     if (validate_ipv4(trimmed_mask)) {
     166            3 :         const prefix = ipaddr.IPv4.parse(trimmed_mask).prefixLengthFromSubnetMask();
     167            3 :         if (prefix !== null) {
     168            3 :             return prefix;
     169            3 :         }
     170            3 :     }
     171              : 
     172            3 :     throw cockpit.format(_("Invalid prefix or netmask $0"), prefix_mask);
     173           10 : }
     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            2 : export const ssid_from_nm = bytes => new TextDecoder().decode(
     271            1 :     new Uint8Array(cockpit.base64_decode(bytes ?? []))
     272            2 : );
     273              : 
     274            2 : export const ssid_to_nm = ssid => cockpit.base64_encode(new TextEncoder().encode(ssid));
     275              : 
     276           44 : export function list_interfaces() {
     277           44 :     return new Promise((resolve, reject) => {
     278           44 :         const client = cockpit.dbus("org.freedesktop.NetworkManager");
     279           44 :         client.call('/org/freedesktop/NetworkManager',
     280           44 :                     'org.freedesktop.NetworkManager',
     281           44 :                     'GetAllDevices', [])
     282           44 :                 .then(reply => {
     283           44 :                     Promise.all(reply[0].map(device => {
     284           44 :                         return Promise.all([
     285           44 :                             client.call(device,
     286           44 :                                         'org.freedesktop.DBus.Properties',
     287           44 :                                         'Get', ['org.freedesktop.NetworkManager.Device', 'Interface'])
     288           44 :                                     .then(reply => reply[0]),
     289           44 :                             client.call(device,
     290           44 :                                         'org.freedesktop.DBus.Properties',
     291           44 :                                         'Get', ['org.freedesktop.NetworkManager.Device', 'Capabilities'])
     292           44 :                                     .then(reply => reply[0])
     293           44 :                         ]);
     294           44 :                     }))
     295           44 :                             .then(interfaces => {
     296           44 :                                 client.close();
     297           44 :                                 resolve(interfaces.map(i => {
     298           44 :                                     return { device: i[0].v, capabilities: i[1].v };
     299           44 :                                 }));
     300           44 :                             })
     301            0 :                             .catch(e => console.warn(JSON.stringify(e)));
     302           44 :                 })
     303            0 :                 .catch(e => console.warn(JSON.stringify(e)));
     304           44 :     });
     305           44 : }
     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           21 : export function isNonPersistentMultiCon(connection) {
     315           19 :     const settings = connection?.Settings;
     316           21 :     if (settings &&
     317           19 :         settings.connection.interface_name === undefined &&
     318            3 :         settings.connection.id === "Wired Connection" &&
     319            2 :         connection.Unsaved === true && settings.connection.type &&
     320            2 :         settings.connection.multi_connect === 3 &&
     321            2 :         settings.connection.type === "802-3-ethernet") {
     322            2 :         return true;
     323            2 :     }
     324              : 
     325           21 :     return false;
     326           21 : }
     327              : 
     328            1 : export function createNewConnSettings(settings, iface_name) {
     329            1 :     const newSettings = {
     330            1 :         ...settings,
     331            1 :         connection: {
     332            1 :             ...settings.connection,
     333            1 :             id: iface_name,
     334            1 :             interface_name: iface_name,
     335              :             // increase the current priority by 1, maximum priority is 999
     336            1 :             autoconnect_priority: Math.min(settings.connection.autoconnect_priority + 1, 999),
     337            1 :         }
     338            1 :     };
     339            1 :     delete newSettings.connection.uuid;
     340            1 :     delete newSettings.connection.multi_connect;
     341              : 
     342            1 :     return newSettings;
     343            1 : }
     344              : 
     345            2 : export function debug() {
     346            1 :     if (window.debugging == "all" || window.debugging?.includes("networkmanager")) // not-covered: debugging
     347            1 :         console.debug("networkmanager:", ...arguments); // not-covered: debugging
     348            2 : }
        

Generated by: LCOV version 2.0-1