matterbridge
Version:
Matterbridge plugin manager for Matter
348 lines • 16 kB
JavaScript
/**
* @description This file contains the class Dgram.
* @file dgram.ts
* @author Luca Liguori
* @created 2025-03-22
* @version 1.0.0
* @license Apache-2.0
*
* Copyright 2025, 2026, 2027 Luca Liguori.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Node.js imports
import dgram from 'node:dgram';
import EventEmitter from 'node:events';
import os from 'node:os';
// AnsiLogger imports
import { AnsiLogger, BLUE, db, idn, nf, rs } from 'node-ansi-logger';
/**
* This class implements a dgram socket.
*/
export class Dgram extends EventEmitter {
log;
socket;
bound = false;
socketType;
interfaceName;
interfaceAddress;
interfaceNetmask;
/**
* Creates an instance of Dgram.
*
* @param {string} name - The name of the socket.
* @param {'udp4' | 'udp6'} socketType - The type of the socket (IPv4 or IPv6).
* @param {boolean | undefined} reuseAddr - Whether to allow address reuse.
* @param {string} [interfaceName] - The name of the network interface to bind to.
* @param {string} [interfaceAddress] - The address of the network interface to bind to.
*/
constructor(name, socketType, reuseAddr = true, interfaceName, interfaceAddress) {
super();
this.log = new AnsiLogger({ logName: name, logTimestampFormat: 4 /* TimestampFormat.TIME_MILLIS */, logLevel: "debug" /* LogLevel.DEBUG */ });
this.socket = dgram.createSocket({ type: socketType, reuseAddr });
this.socketType = socketType;
this.interfaceName = interfaceName;
this.interfaceAddress = interfaceAddress;
this.socket.on('error', (error) => {
this.log.debug(`Socket error: ${error instanceof Error ? error.message : error}`);
this.emit('error', error);
this.onError(error);
});
this.socket.on('close', () => {
this.log.debug('Socket closed');
this.bound = false;
this.emit('close');
this.onClose();
});
this.socket.on('connect', () => {
this.log.info('Socket connected');
this.emit('connect');
this.onConnect();
});
this.socket.on('message', (msg, rinfo) => {
this.log.debug(`Socket received a message from ${BLUE}${rinfo.family}${db} ${BLUE}${rinfo.address}${db}:${BLUE}${rinfo.port}${db}`);
this.emit('message', msg, rinfo);
this.onMessage(msg, rinfo);
});
this.socket.on('listening', () => {
this.bound = true;
const address = this.socket.address();
this.log.debug(`Socket listening on ${BLUE}${address.family}${db} ${BLUE}${address.address}${db}:${BLUE}${address.port}${db}`);
this.emit('listening', address);
this.onListening(address);
});
}
/**
* Sends a message to the specified server.
*
* @param {Buffer} msg - The message buffer to send.
* @param {string} serverAddress - The IPv4 address of the destination server.
* @param {number} serverPort - The port of the destination server.
*/
send(msg, serverAddress, serverPort) {
this.socket.send(msg, 0, msg.length, serverPort, serverAddress, (error) => {
if (error) {
this.log.error(`Socket failed to send a message: ${error instanceof Error ? error.message : error}`);
this.emit('error', error);
this.onError(error);
}
else {
this.log.debug(`Socket sent a message to ${BLUE}${serverAddress}${db}:${BLUE}${serverPort}${db}`);
this.emit('sent', msg, serverAddress, serverPort);
this.onSent(msg, serverAddress, serverPort);
}
});
}
onError(error) {
this.log.error(`Socket error: ${error instanceof Error ? error.message : error}`);
}
onClose() {
this.log.info(`Socket closed`);
}
onConnect() {
this.log.info(`Socket connected`);
}
onSent(msg, serverAddress, serverPort) {
this.log.info(`Socket sent a message to ${BLUE}${serverAddress}${db}:${BLUE}${serverPort}${db}`);
}
onMessage(msg, rinfo) {
this.log.info(`Socket received a message from ${BLUE}${rinfo.family}${nf} ${BLUE}${rinfo.address}${nf}:${BLUE}${rinfo.port}${nf}`);
}
onListening(address) {
this.log.info(`Socket listening on ${BLUE}${address.family}${nf} ${BLUE}${address.address}${nf}:${BLUE}${address.port}${nf}`);
this.onReady(address);
}
onReady(address) {
this.log.info(`Socket ready on ${BLUE}${address.family}${nf} ${BLUE}${address.address}${nf}:${BLUE}${address.port}${nf}`);
this.emit('ready', address);
}
/**
* Retrieves the IPv4 address of the specified network interface or the first external IPv4 interface if no interface is specified.
* Throws an error if no suitable interface or address is found.
*
* @param {string} networkInterface - The name of the network interface to retrieve the IPv4 address from. If not specified, the first external IPv4 interface will be used.
* @returns {string | undefined} The IPv4 address of the specified network interface or the first external IPv4 interface.
* @throws Error if no suitable interface or address is found.
*/
getIpv4InterfaceAddress(networkInterface) {
// Normalize the interface name: treat an empty string as undefined.
if (networkInterface === '')
networkInterface = undefined;
const interfaces = os.networkInterfaces();
// If a specific interface is provided but not found, warn and fall back.
if (networkInterface && !interfaces[networkInterface]) {
this.log.warn(`Interface "${networkInterface}" not found. Using first external IPv4 interface.`);
networkInterface = undefined;
}
// If no interface was specified or the provided one doesn't exist, find the first external IPv4 interface.
if (!networkInterface) {
for (const [interfaceName, interfaceDetails] of Object.entries(interfaces)) {
if (!interfaceDetails)
continue;
// Check if at least one external IPv4 address exists on this interface.
for (const detail of interfaceDetails) {
if (detail.family === 'IPv4' && !detail.internal) {
networkInterface = interfaceName;
break;
}
}
if (networkInterface)
break;
}
}
if (!networkInterface) {
throw new Error(`Didn't find an external IPv4 network interface`);
}
// Select the first external IPv4 address from the interface.
const addresses = interfaces[networkInterface];
const ipv4Address = addresses?.find((addr) => addr.family === 'IPv4' && !addr.internal);
if (!ipv4Address) {
throw new Error(`Interface ${networkInterface} does not have an external IPv4 address`);
}
return ipv4Address.address;
}
/**
* Retrieves the IPv6 address of the specified network interface or the first external IPv6 interface if no interface is specified.
* Throws an error if no suitable interface or address is found.
*
* @param {string} [networkInterface] - The name of the network interface to retrieve the IPv6 address from. If not specified, the first external IPv6 interface will be used.
* @returns {string | undefined} The IPv6 address of the specified network interface or the first external IPv6 interface.
* @throws {Error} If no suitable interface or address is found.
*/
getIpv6InterfaceAddress(networkInterface) {
// Normalize the interface name: treat an empty string as undefined.
if (networkInterface === '')
networkInterface = undefined;
const interfaces = os.networkInterfaces();
// If a specific interface is provided, verify it exists. Otherwise, warn and use the first external IPv6 interface.
if (networkInterface && !interfaces[networkInterface]) {
this.log.warn(`Interface "${networkInterface}" not found. Using first external IPv6 interface.`);
networkInterface = undefined;
}
// If no network interface was specified, search for the first external IPv6 interface.
if (!networkInterface) {
for (const [interfaceName, interfaceDetails] of Object.entries(interfaces)) {
if (!interfaceDetails)
continue;
for (const detail of interfaceDetails) {
if (detail.family === 'IPv6' && !detail.internal) {
networkInterface = interfaceName;
break;
}
}
if (networkInterface)
break;
}
}
if (!networkInterface) {
throw new Error(`Didn't find an external IPv6 network interface`);
}
const addresses = interfaces[networkInterface];
// Try to find a link-local address and use scopeid
const linkLocalAddress = addresses?.find((addr) => addr.family === 'IPv6' && !addr.internal && addr.address.startsWith('fe80'));
if (linkLocalAddress) {
this.log.debug('Found IPv6 link-local address');
return linkLocalAddress.scopeid ? `${linkLocalAddress.address}%${process.platform !== 'win32' ? networkInterface : linkLocalAddress.scopeid}` : linkLocalAddress.address;
}
this.log.debug('No IPv6 link-local address found');
// Try to find a unique local address
const ulaAddress = addresses?.find((addr) => addr.family === 'IPv6' && !addr.internal && addr.address.startsWith('fd') && addr.netmask === 'ffff:ffff:ffff:ffff::');
if (ulaAddress) {
this.log.debug('Found IPv6 Unique Local Addresses (ULA) unicast address');
return ulaAddress.address;
}
this.log.debug('No IPv6 Unique Local Addresses (ULA) unicast address found');
// Try to find a unique local address
const uniqueLocalAddress = addresses?.find((addr) => addr.family === 'IPv6' && !addr.internal && addr.address.startsWith('fd'));
if (uniqueLocalAddress) {
this.log.debug('Found IPv6 Unique Local Addresses (ULA) address');
return uniqueLocalAddress.address;
}
this.log.debug('No IPv6 Unique Local Addresses (ULA) address found');
throw new Error(`Interface ${networkInterface} does not have a suitable external IPv6 address`);
}
/**
* Retrieves the names of all available network interfaces.
*
* @returns {string[]} An array of network interface names.
*/
getInterfacesNames() {
const interfaces = os.networkInterfaces();
const interfaceNames = [];
for (const name in interfaces) {
if (interfaces[name]) {
interfaceNames.push(name);
}
}
return interfaceNames;
}
/**
* Retrieves the scope ID for all interfaces address '::'.
*
* @returns {string} The scope ID of the first found IPv6 address or an empty string.
*/
getIpv6ScopeIdForAllInterfacesAddress() {
const interfaces = os.networkInterfaces();
for (const name in interfaces) {
const iface = interfaces[name];
if (iface) {
const ipv6Address = iface.find((addr) => addr.family === 'IPv6' && !addr.internal && addr.scopeid);
if (ipv6Address) {
return process.platform === 'win32' ? '%' + String(ipv6Address.scopeid) : '%' + name; // Use the scope ID for Windows, or the interface name for non-Windows platforms
}
}
}
return '';
}
/**
* Retrieves the interface name from the scope id of an IPv6 address.
*
* @param {number} scopeId - The scope id of the IPv6 address.
* @returns {string | undefined} The interface name or undefined if not found.
*/
getInterfaceNameFromScopeId(scopeId) {
const nets = os.networkInterfaces();
for (const ifaceName in nets) {
const addresses = nets[ifaceName] || [];
for (const addr of addresses) {
// Check for IPv6 addresses with a matching scope id.
if (addr.family === 'IPv6' && addr.scopeid === scopeId) {
return ifaceName;
}
}
}
return undefined;
}
/**
* Retrieves the netmask of the specified interface address.
*
* @param {string} interfaceAddress - The interface address for which to retrieve the netmask.
* @returns {string | undefined} The netmask of the specified interface address or undefined if not found.
*/
getNetmask(interfaceAddress) {
// Remove zone index if present (e.g. for IPv6 "fe80::1%eth0")
const cleanedAddress = interfaceAddress.includes('%') ? interfaceAddress.split('%')[0] : interfaceAddress;
// Iterate over all interfaces.
const nets = os.networkInterfaces();
for (const ifaceName in nets) {
const ifaceAddresses = nets[ifaceName];
if (!ifaceAddresses)
continue;
for (const addr of ifaceAddresses) {
if (addr.address === cleanedAddress) {
return addr.netmask;
}
}
}
return undefined;
}
/**
* Computes the broadcast address given an IPv4 address and netmask.
*
* @param {string | undefined} [ipAddress] - The IPv4 address e.g. "192.168.1.20"
* @param {string | undefined} [netmask] - The IPv4 netmask e.g. "255.255.255.0"
* @returns {string | undefined} The computed broadcast address, e.g. "192.168.1.255"
*/
getIpv4BroadcastAddress(ipAddress, netmask) {
if (!ipAddress || !netmask) {
return undefined;
}
const ipParts = ipAddress.split('.').map(Number);
const maskParts = netmask.split('.').map(Number);
const broadcastParts = ipParts.map((octet, i) => (octet & maskParts[i]) | (255 - maskParts[i]));
return broadcastParts.join('.');
}
/**
* Returns the broadcast IPv6 address.
*
* @returns {string} The broadcast IPv6 address, e.g. "ff02::1"
*/
getIpv6BroadcastAddress() {
return 'ff02::1';
}
/**
* Logs all available network interfaces and their details.
*/
listNetworkInterfaces() {
const interfaces = os.networkInterfaces();
for (const [name, addresses] of Object.entries(interfaces)) {
if (!addresses)
continue;
this.log.debug(`Interface: ${idn}${name}${rs}${db}`);
for (const address of addresses) {
this.log.debug(`- address ${BLUE}${address.address}${db} netmask ${BLUE}${address.netmask}${db} ${address.mac ? 'MAC: ' + BLUE + address.mac + db : ''} type: ${BLUE}${address.family}${db} ${BLUE}${address.internal ? 'internal' : 'external'}${db} ${address.scopeid !== undefined ? 'scopeid: ' + BLUE + address.scopeid + db : ''} cidr: ${BLUE}${address.cidr}${db}`);
}
}
}
}
//# sourceMappingURL=dgram.js.map