UNPKG

7.69 kBPlain TextView Raw
1import os from 'node:os';
2import fs from 'node:fs/promises';
3import childProcess from 'node:child_process';
4
5const DEFAULT_RESOLV_FILE = '/etc/resolv.conf';
6
7// osx start line 'en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500'
8// linux start line 'eth0 Link encap:Ethernet HWaddr 00:16:3E:00:0A:29 '
9const MAC_OSX_START_LINE = /^(\w+)\:\s+flags=/;
10const MAC_LINUX_START_LINE = /^(\w+)\s{2,}link encap:\w+/i;
11
12// ether 78:ca:39:b0:e6:7d
13// HWaddr 00:16:3E:00:0A:29
14export const MAC_RE = /(?:ether|HWaddr)\s+((?:[a-z0-9]{2}\:){5}[a-z0-9]{2})/i;
15
16// osx: inet 192.168.2.104 netmask 0xffffff00 broadcast 192.168.2.255
17// linux: inet addr:10.125.5.202 Bcast:10.125.15.255 Mask:255.255.240.0
18export const MAC_IP_RE = /inet\s(?:addr\:)?(\d+\.\d+\.\d+\.\d+)/;
19
20export interface Address {
21 ip?: string;
22 ipv6?: string;
23 mac?: string;
24}
25
26export type AddressCallback = (err: Error | null, addr: Address) => void;
27export type MacCallback = (err?: Error | null, addr?: string | null) => void;
28export type DnsCallback = (err?: Error | null, servers?: string[]) => void;
29
30function getDefaultInterfaceName() {
31 let val: string | undefined = 'eth';
32 const platform = os.platform();
33 if (platform === 'darwin') {
34 val = 'en';
35 } else if (platform === 'win32') {
36 val = undefined;
37 }
38 return val;
39}
40
41function getIfconfigCMD() {
42 if (os.platform() === 'win32') {
43 return 'ipconfig/all';
44 }
45 return '/sbin/ifconfig';
46}
47
48// typeof os.networkInterfaces family is a number (v18.0.0)
49// types: 'IPv4' | 'IPv6' => 4 | 6
50// @see https://github.com/nodejs/node/issues/42861
51function matchName(actualFamily: string | number, expectedFamily: string | number) {
52 if (expectedFamily === 'IPv4') {
53 return actualFamily === 'IPv4' || actualFamily === 4;
54 }
55 if (expectedFamily === 'IPv6') {
56 return actualFamily === 'IPv6' || actualFamily === 6;
57 }
58 return actualFamily === expectedFamily;
59}
60
61export function getInterfaceAddress(family?: string, name?: string) {
62 const interfaces = os.networkInterfaces();
63 const noName = !name;
64 name = name || getDefaultInterfaceName();
65 family = family || 'IPv4';
66 if (name) {
67 for (let i = -1; i < 8; i++) {
68 const interfaceName = name + (i >= 0 ? i : ''); // support 'lo' and 'lo0'
69 const items = interfaces[interfaceName];
70 if (items) {
71 for (const item of items) {
72 if (matchName(item.family, family)) {
73 return item;
74 }
75 }
76 }
77 }
78 }
79
80 if (noName) {
81 // filter all loopback or local addresses
82 for (const k in interfaces) {
83 const items = interfaces[k];
84 if (items) {
85 for (const item of items) {
86 // all 127 addresses are local and should be ignored
87 if (matchName(item.family, family) && !item.address.startsWith('127.')) {
88 return item;
89 }
90 }
91 }
92 }
93 }
94 return;
95}
96
97/**
98 * Get current machine IPv4
99 *
100 * interfaceName: interface name, default is 'eth' on linux, 'en' on mac os.
101 */
102export function ip(interfaceName?: string) {
103 const item = getInterfaceAddress('IPv4', interfaceName);
104 return item?.address;
105}
106
107/**
108 * Get current machine IPv6
109 *
110 * interfaceName: interface name, default is 'eth' on linux, 'en' on mac os.
111 */
112export function ipv6(interfaceName?: string) {
113 const item = getInterfaceAddress('IPv6', interfaceName);
114 return item?.address;
115}
116
117function getMAC(content: string, interfaceName: string, matchIP: string) {
118 const lines = content.split('\n');
119 for (let i = 0; i < lines.length; i++) {
120 let line = lines[i].trimEnd();
121 const m = MAC_OSX_START_LINE.exec(line) || MAC_LINUX_START_LINE.exec(line);
122 if (!m) {
123 continue;
124 }
125
126 // check interface name
127 const name = m[1];
128 if (name.indexOf(interfaceName) !== 0) {
129 continue;
130 }
131
132 let ip: string | null = null;
133 let mac: string | null = null;
134 let match = MAC_RE.exec(line);
135 if (match) {
136 mac = match[1];
137 }
138
139 i++;
140 // eslint-disable-next-line no-constant-condition
141 while (true) {
142 line = lines[i];
143 if (!line || MAC_OSX_START_LINE.exec(line) || MAC_LINUX_START_LINE.exec(line)) {
144 i--;
145 break; // hit next interface, handle next interface
146 }
147 if (!mac) {
148 match = MAC_RE.exec(line);
149 if (match) {
150 mac = match[1];
151 }
152 }
153
154 if (!ip) {
155 match = MAC_IP_RE.exec(line);
156 if (match) {
157 ip = match[1];
158 }
159 }
160
161 i++;
162 }
163
164 if (ip === matchIP) {
165 return mac;
166 }
167 }
168 return null;
169}
170
171/**
172 * Get current machine MAC address
173 *
174 * interfaceName: name, default is 'eth' on linux, 'en' on mac os.
175 */
176export function mac(callback: MacCallback): void;
177export function mac(interfaceName: string, callback: MacCallback): void;
178export function mac(interfaceNameOrCallback: string | MacCallback, callback?: MacCallback) {
179 let interfaceName: string | undefined;
180 if (typeof interfaceNameOrCallback === 'function') {
181 callback = interfaceNameOrCallback;
182 } else {
183 interfaceName = interfaceNameOrCallback;
184 }
185 interfaceName = interfaceName || getDefaultInterfaceName();
186 const item = getInterfaceAddress('IPv4', interfaceName);
187 if (!item) {
188 return callback!();
189 }
190
191 // https://github.com/nodejs/node/issues/13581
192 // bug in node 7.x and <= 8.4.0
193 if (!process.env.CI && (item.mac === 'ff:00:00:00:00:00' || item.mac === '00:00:00:00:00:00')) {
194 // wrong address, ignore it
195 item.mac = '';
196 }
197
198 if (item.mac) {
199 return callback!(null, item.mac);
200 }
201
202 childProcess.exec(getIfconfigCMD(), { timeout: 5000 }, (err, stdout) => {
203 if (err || !stdout) {
204 return callback!(err);
205 }
206
207 if (!interfaceName) {
208 return callback!();
209 }
210 const mac = getMAC(stdout || '', interfaceName, item.address);
211 callback!(null, mac);
212 });
213}
214
215// nameserver 172.24.102.254
216const DNS_SERVER_RE = /^nameserver\s+(\d+\.\d+\.\d+\.\d+)$/i;
217
218/**
219 * Get DNS servers.
220 *
221 * filepath: resolv config file path. default is '/etc/resolv.conf'.
222 */
223export function dns(callback: DnsCallback): void;
224export function dns(filepath: string, callback: DnsCallback): void;
225export function dns(filepathOrCallback: string | DnsCallback, callback?: DnsCallback) {
226 let filepath: string | undefined;
227 if (typeof filepathOrCallback === 'function') {
228 callback = filepathOrCallback;
229 } else {
230 filepath = filepathOrCallback;
231 }
232 filepath = filepath || DEFAULT_RESOLV_FILE;
233 fs.readFile(filepath, 'utf8')
234 .then(content => {
235 const servers: string[] = [];
236 content = content || '';
237 const lines = content.split('\n');
238 for (const line of lines) {
239 const m = DNS_SERVER_RE.exec(line.trim());
240 if (m) {
241 servers.push(m[1]);
242 }
243 }
244 callback!(null, servers);
245 })
246 .catch(err => {
247 callback!(err);
248 });
249}
250
251/**
252 * Get all addresses.
253 *
254 * interfaceName: interface name, default is 'eth' on linux, 'en' on mac os.
255 */
256export function address(callback: AddressCallback): void;
257export function address(interfaceName: string, callback: AddressCallback): void;
258export function address(interfaceNameOrCallback: string | AddressCallback, callback?: AddressCallback) {
259 let interfaceName: string | undefined;
260 if (typeof interfaceNameOrCallback === 'function') {
261 callback = interfaceNameOrCallback;
262 } else {
263 interfaceName = interfaceNameOrCallback;
264 }
265
266 const addr: Address = {
267 ip: ip(interfaceName),
268 ipv6: ipv6(interfaceName),
269 mac: undefined,
270 };
271 mac(interfaceName || '', (err?: Error | null, mac?: string | null) => {
272 if (mac) {
273 addr.mac = mac;
274 }
275 callback!(err || null, addr);
276 });
277}