mirror of
https://github.com/cockpit-project/cockpit.git
synced 2026-11-03 21:49:50 -09:00
370 lines
12 KiB
TypeScript
370 lines
12 KiB
TypeScript
/*
|
|
* Copyright (C) 2016 Red Hat, Inc.
|
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
|
*/
|
|
|
|
import cockpit from "cockpit";
|
|
import * as ipaddr from "ipaddr.js";
|
|
|
|
const _ = cockpit.gettext;
|
|
|
|
type IPAddress = ipaddr.IPv4 | ipaddr.IPv6;
|
|
|
|
type NetworkManagerSettings = {
|
|
connection: {
|
|
id: string;
|
|
uuid?: string;
|
|
interface_name: string | undefined;
|
|
multi_connect?: number;
|
|
type: string
|
|
autoconnect_priority: number;
|
|
}
|
|
}
|
|
|
|
type NetworkManagerConnection = {
|
|
Settings: NetworkManagerSettings;
|
|
Unsaved: boolean;
|
|
}
|
|
|
|
/* NetworkManager specific data conversions and utility functions.
|
|
*/
|
|
|
|
type ByteOrder = "be" | "le" | undefined;
|
|
let byteorder: ByteOrder;
|
|
|
|
export function set_byteorder(bo: ByteOrder) {
|
|
byteorder = bo;
|
|
}
|
|
|
|
export function ip_prefix_to_text(num: number): string {
|
|
return num.toString();
|
|
}
|
|
|
|
export function ip_prefix_from_text(text: string): number {
|
|
if (/^[0-9]+$/.test(text.trim()))
|
|
return parseInt(text, 10);
|
|
|
|
throw cockpit.format(_("Invalid prefix $0"), text);
|
|
}
|
|
|
|
export function ip_metric_to_text(num: number): string {
|
|
return num.toString();
|
|
}
|
|
|
|
export function ip_metric_from_text(text: string): number {
|
|
if (text === "")
|
|
return 0;
|
|
|
|
if (/^[0-9]+$/.test(text.trim()))
|
|
return parseInt(text, 10);
|
|
|
|
throw cockpit.format(_("Invalid metric $0"), text);
|
|
}
|
|
|
|
export function ip_network_address(address: IPAddress, prefix_len: number): string | null {
|
|
const addrCIDR = address.toString() + "/" + prefix_len;
|
|
const ip_obj = address.kind() === "ipv4" ? ipaddr.IPv4 : ipaddr.IPv6;
|
|
|
|
try {
|
|
return ip_obj.networkAddressFromCIDR(addrCIDR).toString();
|
|
} catch (_e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function ip_first_usable_address(address: IPAddress, prefix: number): string | null {
|
|
try {
|
|
const addr_cidr = address.toString() + "/" + prefix;
|
|
|
|
if (address.kind() === "ipv4") {
|
|
const netAddr = ipaddr.IPv4.networkAddressFromCIDR(addr_cidr);
|
|
netAddr.octets[3] += (prefix < 31) ? 1 : 0;
|
|
return netAddr.toString();
|
|
} else {
|
|
const netAddr = ipaddr.IPv6.networkAddressFromCIDR(addr_cidr);
|
|
netAddr.parts[7] += (prefix < 127) ? 1 : 0;
|
|
return netAddr.toString();
|
|
}
|
|
} catch (_e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function toDec(n: number): string {
|
|
return n.toString(10);
|
|
}
|
|
|
|
function bytes_from_nm32(num: number): number[] {
|
|
const bytes = [];
|
|
if (byteorder === "be") {
|
|
for (let i = 3; i >= 0; i--) {
|
|
bytes[i] = num & 0xFF;
|
|
num = num >>> 8;
|
|
}
|
|
} else if (byteorder === "le") {
|
|
for (let i = 0; i < 4; i++) {
|
|
bytes[i] = num & 0xFF;
|
|
num = num >>> 8;
|
|
}
|
|
} else {
|
|
throw new Error("byteorder is unset or has invalid value " + JSON.stringify(byteorder));
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
export function validate_ipv4(address: string): boolean {
|
|
// explicitly require all 4 octets
|
|
// NM does not support any IPv4 short format
|
|
return ipaddr.IPv4.isValidFourPartDecimal(address);
|
|
}
|
|
|
|
export function validate_ipv6(address: string): boolean {
|
|
return ipaddr.IPv6.isValid(address);
|
|
}
|
|
|
|
export function validate_ip(address: string): boolean {
|
|
return validate_ipv4(address) || validate_ipv6(address);
|
|
}
|
|
|
|
export function ip4_to_text(num: number, zero_is_empty?: boolean): string {
|
|
if (num === 0 && zero_is_empty)
|
|
return "";
|
|
return bytes_from_nm32(num).map(toDec)
|
|
.join('.');
|
|
}
|
|
|
|
export function ip4_from_text(text: string, empty_is_zero?: boolean): number {
|
|
function invalid(): never {
|
|
throw cockpit.format(_("Invalid address $0"), text);
|
|
}
|
|
|
|
if (text === "" && empty_is_zero)
|
|
return 0;
|
|
|
|
const parts = text.split('.');
|
|
if (parts.length !== 4)
|
|
invalid();
|
|
|
|
const bytes = parts.map(function(s) {
|
|
if (/^[0-9]+$/.test(s.trim()))
|
|
return parseInt(s, 10);
|
|
else
|
|
return invalid();
|
|
});
|
|
|
|
let num = 0;
|
|
function shift(b: number) {
|
|
if (isNaN(b) || b < 0 || b > 0xFF)
|
|
invalid();
|
|
num = 0x100 * num + b;
|
|
}
|
|
|
|
if (byteorder === "be") {
|
|
for (let i = 0; i < 4; i++) {
|
|
shift(bytes[i]);
|
|
}
|
|
} else if (byteorder === "le") {
|
|
for (let i = 3; i >= 0; i--) {
|
|
shift(bytes[i]);
|
|
}
|
|
} else {
|
|
throw new Error("byteorder is unset or has invalid value " + JSON.stringify(byteorder));
|
|
}
|
|
|
|
return num;
|
|
}
|
|
|
|
export function ip4_prefix_from_text(prefix_mask: string): number {
|
|
const trimmed_mask = prefix_mask.trim();
|
|
if (/^[0-9]+$/.test(trimmed_mask)) {
|
|
return parseInt(prefix_mask, 10);
|
|
}
|
|
|
|
// make sure that mask has 4 octets format
|
|
if (validate_ipv4(trimmed_mask)) {
|
|
const prefix = ipaddr.IPv4.parse(trimmed_mask).prefixLengthFromSubnetMask();
|
|
if (prefix !== null) {
|
|
return prefix;
|
|
}
|
|
}
|
|
|
|
throw cockpit.format(_("Invalid prefix or netmask $0"), prefix_mask);
|
|
}
|
|
|
|
// Shorten IPv6 address according to RFC 5952
|
|
// https://datatracker.ietf.org/doc/html/rfc5952#section-4
|
|
//
|
|
// NetworkManager already handles dropping of leading zeros within a single 16 bit field
|
|
// but does not replace the longest consecutive zeros fields with "::".
|
|
function ip6_shorten(ip6_addr: string): string {
|
|
function find_longest_zero(match: RegExpMatchArray[]): number {
|
|
let idx = -1;
|
|
let length = 0;
|
|
|
|
match.forEach((item, i) => {
|
|
const count_zero = item[0].replaceAll(':', '').length;
|
|
if (count_zero > length) {
|
|
idx = i;
|
|
length = count_zero;
|
|
}
|
|
});
|
|
|
|
return idx;
|
|
}
|
|
|
|
const REGEX_MATCH_CONSECUTIVE_ZEROS = /\b:?(?:0:?){2,}/g;
|
|
const match = [...ip6_addr.matchAll(REGEX_MATCH_CONSECUTIVE_ZEROS)];
|
|
|
|
// nothing to shorten
|
|
if (match.length === 0) {
|
|
return ip6_addr;
|
|
}
|
|
|
|
const longest_idx = find_longest_zero(match);
|
|
// replace first (leftmost) match
|
|
const short_addr = ip6_addr.replace(match[longest_idx][0], "::");
|
|
|
|
return short_addr;
|
|
}
|
|
|
|
export function ip6_to_text(data: string, zero_is_empty?: boolean): string {
|
|
const parts = [];
|
|
const bytes = cockpit.base64_decode(data);
|
|
for (let i = 0; i < 8; i++)
|
|
parts[i] = ((bytes[2 * i] << 8) + bytes[2 * i + 1]).toString(16);
|
|
const result = parts.join(':');
|
|
if (result === "0:0:0:0:0:0:0:0" && zero_is_empty)
|
|
return "";
|
|
return ip6_shorten(result);
|
|
}
|
|
|
|
export function ip6_from_text(text: string, empty_is_zero?: boolean): string {
|
|
function invalid() {
|
|
throw cockpit.format(_("Invalid address $0"), text);
|
|
}
|
|
|
|
if (text === "" && empty_is_zero)
|
|
return cockpit.base64_encode([0, 0, 0, 0, 0, 0, 0, 0,
|
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
]);
|
|
|
|
const parts = text.split(':');
|
|
if (parts.length < 1 || parts.length > 8)
|
|
invalid();
|
|
|
|
if (parts[0] === "")
|
|
parts[0] = "0";
|
|
if (parts[parts.length - 1] === "")
|
|
parts[parts.length - 1] = "0";
|
|
|
|
const bytes = [];
|
|
let empty_seen = false;
|
|
let j = 0;
|
|
for (let i = 0; i < parts.length; i++, j++) {
|
|
if (parts[i] === "") {
|
|
if (empty_seen)
|
|
invalid();
|
|
empty_seen = true;
|
|
while (j < i + (8 - parts.length)) {
|
|
bytes[2 * j] = bytes[2 * j + 1] = 0;
|
|
j++;
|
|
}
|
|
} else {
|
|
if (!/^[0-9a-fA-F]+$/.test(parts[i].trim()))
|
|
invalid();
|
|
const n = parseInt(parts[i], 16);
|
|
if (isNaN(n) || n < 0 || n > 0xFFFF)
|
|
invalid();
|
|
bytes[2 * j] = n >> 8;
|
|
bytes[2 * j + 1] = n & 0xFF;
|
|
}
|
|
}
|
|
if (j !== 8)
|
|
invalid();
|
|
|
|
return cockpit.base64_encode(bytes);
|
|
}
|
|
|
|
// SSID comes as a base64-encoded string from D-Bus (signature 'ay')
|
|
export const ssid_from_nm = (bytes: string | null | undefined): string => new TextDecoder().decode(
|
|
new Uint8Array(cockpit.base64_decode(bytes ?? ""))
|
|
);
|
|
|
|
export const ssid_to_nm = (ssid: string): string => cockpit.base64_encode(new TextEncoder().encode(ssid));
|
|
|
|
export function list_interfaces() {
|
|
return new Promise((resolve, _reject) => {
|
|
const client = cockpit.dbus("org.freedesktop.NetworkManager");
|
|
client.call('/org/freedesktop/NetworkManager',
|
|
'org.freedesktop.NetworkManager',
|
|
'GetAllDevices', [])
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
.then((reply: any[]) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
Promise.all(reply[0].map((device: any) => {
|
|
return Promise.all([
|
|
client.call(device,
|
|
'org.freedesktop.DBus.Properties',
|
|
'Get', ['org.freedesktop.NetworkManager.Device', 'Interface'])
|
|
.then(reply => reply[0]),
|
|
client.call(device,
|
|
'org.freedesktop.DBus.Properties',
|
|
'Get', ['org.freedesktop.NetworkManager.Device', 'Capabilities'])
|
|
.then(reply => reply[0])
|
|
]);
|
|
}))
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
.then((interfaces: any[]) => {
|
|
client.close();
|
|
resolve(interfaces.map(i => {
|
|
return { device: i[0].v, capabilities: i[1].v };
|
|
}));
|
|
})
|
|
.catch(e => console.warn(JSON.stringify(e)));
|
|
})
|
|
.catch(e => console.warn(JSON.stringify(e)));
|
|
});
|
|
}
|
|
|
|
// Check if we are working with non-persistent multi-connection,
|
|
// namely the `id=Wired Connection` generated by networkmanager.
|
|
//
|
|
// When editing this connection, we need to create a new connection
|
|
// on top of this, remove the multiconnection settings and assign it directly
|
|
// to interface. This is done for Anaconda in order to avoid creating unexpected
|
|
// multiconnections when user modifies the connection during installation.
|
|
export function isNonPersistentMultiCon(connection: NetworkManagerConnection): boolean {
|
|
const settings = connection?.Settings;
|
|
if (settings &&
|
|
settings.connection.interface_name === undefined &&
|
|
settings.connection.id === "Wired Connection" &&
|
|
connection.Unsaved === true && settings.connection.type &&
|
|
settings.connection.multi_connect === 3 &&
|
|
settings.connection.type === "802-3-ethernet") {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function createNewConnSettings(settings: NetworkManagerSettings, iface_name: string) {
|
|
const newSettings = {
|
|
...settings,
|
|
connection: {
|
|
...settings.connection,
|
|
id: iface_name,
|
|
interface_name: iface_name,
|
|
// increase the current priority by 1, maximum priority is 999
|
|
autoconnect_priority: Math.min(settings.connection.autoconnect_priority + 1, 999),
|
|
}
|
|
};
|
|
delete newSettings.connection.uuid;
|
|
delete newSettings.connection.multi_connect;
|
|
|
|
return newSettings;
|
|
}
|
|
|
|
export function debug(...args: Parameters<typeof console.debug>): void {
|
|
if (window.debugging === "all" || window.debugging?.includes("networkmanager")) // not-covered: debugging
|
|
console.debug("networkmanager:", ...args); // not-covered: debugging
|
|
}
|