@homebridge/ciao
Version:
ciao is a RFC 6763 compliant dns-sd library, advertising on multicast dns (RFC 6762) implemented in plain Typescript/JavaScript
690 lines • 32.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NetworkManager = exports.NetworkManagerEvent = exports.WifiState = exports.IPFamily = void 0;
const tslib_1 = require("tslib");
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
const assert_1 = tslib_1.__importDefault(require("assert"));
const child_process_1 = tslib_1.__importDefault(require("child_process"));
const debug_1 = tslib_1.__importDefault(require("debug"));
const events_1 = require("events");
const fast_deep_equal_1 = tslib_1.__importDefault(require("fast-deep-equal"));
const net_1 = tslib_1.__importDefault(require("net"));
const os_1 = tslib_1.__importDefault(require("os"));
const domain_formatter_1 = require("./util/domain-formatter");
const debug = (0, debug_1.default)("ciao:NetworkManager");
var IPFamily;
(function (IPFamily) {
IPFamily["IPv4"] = "IPv4";
IPFamily["IPv6"] = "IPv6";
})(IPFamily || (exports.IPFamily = IPFamily = {}));
var WifiState;
(function (WifiState) {
WifiState[WifiState["UNDEFINED"] = 0] = "UNDEFINED";
WifiState[WifiState["NOT_A_WIFI_INTERFACE"] = 1] = "NOT_A_WIFI_INTERFACE";
WifiState[WifiState["NOT_ASSOCIATED"] = 2] = "NOT_ASSOCIATED";
WifiState[WifiState["CONNECTED"] = 3] = "CONNECTED";
})(WifiState || (exports.WifiState = WifiState = {}));
var NetworkManagerEvent;
(function (NetworkManagerEvent) {
NetworkManagerEvent["NETWORK_UPDATE"] = "network-update";
})(NetworkManagerEvent || (exports.NetworkManagerEvent = NetworkManagerEvent = {}));
/**
* The NetworkManager maintains a representation of the network interfaces define on the host system.
* It periodically checks for updated network information.
*
* The NetworkManager makes the following decision when checking for interfaces:
* * First of all it gathers the default network interface of the system (by checking the routing table of the os)
* * The following interfaces are going to be tracked:
* * The loopback interface
* * All interfaces which match the subnet of the default interface
* * All interfaces which contain a globally unique (aka globally routable) ipv6 address
*/
class NetworkManager extends events_1.EventEmitter {
constructor(options) {
super();
this.currentInterfaces = new Map();
/**
* A subset of our network interfaces, holding only loopback interfaces (or what node considers "internal").
*/
this.loopbackInterfaces = new Map();
this.setMaxListeners(100); // we got one listener for every Responder, 100 should be fine for now
if (options && options.interface) {
let interfaces;
if (typeof options.interface === "string") {
interfaces = [options.interface];
}
else if (Array.isArray(options.interface)) {
interfaces = options.interface;
}
else {
throw new Error("Found invalid type for 'interfaces' NetworkManager option!");
}
const restrictedInterfaces = [];
for (const iface of interfaces) {
if (net_1.default.isIP(iface)) {
const interfaceName = NetworkManager.resolveInterface(iface);
if (interfaceName) {
restrictedInterfaces.push(interfaceName);
}
else {
console.log("CIAO: Interface was specified as ip (%s), though couldn't find a matching interface for the given address.", options.interface);
}
}
else {
restrictedInterfaces.push(iface);
}
}
if (restrictedInterfaces.length === 0) {
console.log("CIAO: 'restrictedInterfaces' array was empty. Going to fallback to bind on all available interfaces.");
}
else {
this.restrictedInterfaces = restrictedInterfaces;
}
}
this.excludeIpv6 = !!(options && options.excludeIpv6);
this.excludeIpv6Only = this.excludeIpv6 || !!(options && options.excludeIpv6Only);
if (options) {
debug("Created NetworkManager with options: %s", JSON.stringify(options));
}
this.initPromise = new Promise(resolve => {
this.getCurrentNetworkInterfaces().then(map => {
this.currentInterfaces = map;
const otherInterfaces = Object.keys(os_1.default.networkInterfaces());
const interfaceNames = [];
for (const name of this.currentInterfaces.keys()) {
interfaceNames.push(name);
const index = otherInterfaces.indexOf(name);
if (index !== -1) {
otherInterfaces.splice(index, 1);
}
}
debug("Initial networks [%s] ignoring [%s]", interfaceNames.join(", "), otherInterfaces.join(", "));
this.initPromise = undefined;
resolve();
this.scheduleNextJob();
});
});
}
async waitForInit() {
if (this.initPromise) {
await this.initPromise;
}
}
shutdown() {
if (this.currentTimer) {
clearTimeout(this.currentTimer);
this.currentTimer = undefined;
}
this.removeAllListeners();
}
getInterfaceMap() {
if (this.initPromise) {
assert_1.default.fail("Not yet initialized!");
}
return this.currentInterfaces;
}
getInterface(name) {
if (this.initPromise) {
assert_1.default.fail("Not yet initialized!");
}
return this.currentInterfaces.get(name);
}
isLoopbackNetaddressV4(netaddress) {
for (const networkInterface of this.loopbackInterfaces.values()) {
if (networkInterface.ipv4Netaddress === netaddress) {
return true;
}
}
return false;
}
scheduleNextJob() {
this.currentTimer = setTimeout(this.checkForNewInterfaces.bind(this), NetworkManager.POLLING_TIME);
this.currentTimer.unref(); // this timer won't prevent shutdown
}
async checkForNewInterfaces() {
const latestInterfaces = await this.getCurrentNetworkInterfaces();
if (!this.currentTimer) { // if the timer is undefined, NetworkManager was shut down
return;
}
let added = undefined;
let removed = undefined;
let changes = undefined;
for (const [name, networkInterface] of latestInterfaces) {
const currentInterface = this.currentInterfaces.get(name);
if (currentInterface) { // the interface could potentially have changed
if (!(0, fast_deep_equal_1.default)(currentInterface, networkInterface)) {
// indeed the interface changed
const change = {
name: name,
};
if (currentInterface.ipv4 !== networkInterface.ipv4) { // check for changed ipv4
if (currentInterface.ipv4) {
change.outdatedIpv4 = currentInterface.ipv4;
}
if (networkInterface.ipv4) {
change.updatedIpv4 = networkInterface.ipv4;
}
}
if (currentInterface.ipv6 !== networkInterface.ipv6) { // check for changed link-local ipv6
if (currentInterface.ipv6) {
change.outdatedIpv6 = currentInterface.ipv6;
}
if (networkInterface.ipv6) {
change.updatedIpv6 = networkInterface.ipv6;
}
}
if (currentInterface.globallyRoutableIpv6 !== networkInterface.globallyRoutableIpv6) { // check for changed routable ipv6
if (currentInterface.globallyRoutableIpv6) {
change.outdatedGloballyRoutableIpv6 = currentInterface.globallyRoutableIpv6;
}
if (networkInterface.globallyRoutableIpv6) {
change.updatedGloballyRoutableIpv6 = networkInterface.globallyRoutableIpv6;
}
}
if (currentInterface.uniqueLocalIpv6 !== networkInterface.uniqueLocalIpv6) { // check for changed ula
if (currentInterface.uniqueLocalIpv6) {
change.outdatedUniqueLocalIpv6 = currentInterface.uniqueLocalIpv6;
}
if (networkInterface.uniqueLocalIpv6) {
change.updatedUniqueLocalIpv6 = networkInterface.uniqueLocalIpv6;
}
}
this.currentInterfaces.set(name, networkInterface);
if (networkInterface.loopback) {
this.loopbackInterfaces.set(name, networkInterface);
}
(changes !== null && changes !== void 0 ? changes : (changes = [])).push(change);
}
}
else { // new interface was added/started
this.currentInterfaces.set(name, networkInterface);
if (networkInterface.loopback) {
this.loopbackInterfaces.set(name, networkInterface);
}
(added !== null && added !== void 0 ? added : (added = [])).push(networkInterface);
}
}
// at this point we updated any existing interfaces and added all new interfaces
// thus if the length of below is not the same interface must have been removed
// this check ensures that we do not unnecessarily loop twice through our interfaces
if (this.currentInterfaces.size !== latestInterfaces.size) {
for (const [name, networkInterface] of this.currentInterfaces) {
if (!latestInterfaces.has(name)) { // interface was removed
this.currentInterfaces.delete(name);
this.loopbackInterfaces.delete(name);
(removed !== null && removed !== void 0 ? removed : (removed = [])).push(networkInterface);
}
}
}
if (added || removed || changes) { // emit an event only if anything changed
const addedString = added ? added.map(iface => iface.name).join(",") : "";
const removedString = removed ? removed.map(iface => iface.name).join(",") : "";
const changesString = changes ? changes.map(iface => {
let string = `{ name: ${iface.name} `;
if (iface.outdatedIpv4 || iface.updatedIpv4) {
string += `, ${iface.outdatedIpv4} -> ${iface.updatedIpv4} `;
}
if (iface.outdatedIpv6 || iface.updatedIpv6) {
string += `, ${iface.outdatedIpv6} -> ${iface.updatedIpv6} `;
}
if (iface.outdatedGloballyRoutableIpv6 || iface.updatedGloballyRoutableIpv6) {
string += `, ${iface.outdatedGloballyRoutableIpv6} -> ${iface.updatedGloballyRoutableIpv6} `;
}
if (iface.outdatedUniqueLocalIpv6 || iface.updatedUniqueLocalIpv6) {
string += `, ${iface.outdatedUniqueLocalIpv6} -> ${iface.updatedUniqueLocalIpv6} `;
}
return string + "}";
}).join(",") : "";
debug("Detected network changes: added: [%s], removed: [%s], changes: [%s]!", addedString, removedString, changesString);
this.emit("network-update" /* NetworkManagerEvent.NETWORK_UPDATE */, {
added: added,
removed: removed,
changes: changes,
});
}
this.scheduleNextJob();
}
async getCurrentNetworkInterfaces() {
let names;
if (this.restrictedInterfaces) {
names = this.restrictedInterfaces;
const loopback = NetworkManager.getLoopbackInterface();
if (!names.includes(loopback)) {
names.push(loopback);
}
}
else {
try {
names = await NetworkManager.getNetworkInterfaceNames();
}
catch (error) {
debug(`WARNING Detecting network interfaces for platform '${os_1.default.platform()}' failed. Trying to assume network interfaces! (${error.message})`);
// fallback way of gathering network interfaces (remember, there are docker images where the arp command is not installed)
names = NetworkManager.assumeNetworkInterfaceNames();
}
}
const interfaces = new Map();
const networkInterfaces = os_1.default.networkInterfaces();
for (const name of names) {
const infos = networkInterfaces[name];
if (!infos) {
continue;
}
let ipv4Info = undefined;
let ipv6Info = undefined;
let routableIpv6Info = undefined;
let uniqueLocalIpv6Info = undefined;
let internal = false;
for (const info of infos) {
if (info.internal) {
internal = true;
}
// @ts-expect-error Nodejs 18+ uses the number 4 instead of the string "IPv4"
if ((info.family === "IPv4" || info.family === 4) && !ipv4Info) {
ipv4Info = info;
// @ts-expect-error Nodejs 18+ uses the number 4 instead of the string "IPv4"
}
else if (info.family === "IPv6" || info.family === 6) {
if (this.excludeIpv6) {
continue;
}
if (info.scopeid && !ipv6Info) { // we only care about non zero scope (aka link-local ipv6)
ipv6Info = info;
}
else if (info.scopeid === 0) { // global routable ipv6
if (info.address.startsWith("fc") || info.address.startsWith("fd")) {
if (!uniqueLocalIpv6Info) {
uniqueLocalIpv6Info = info;
}
}
else if (!routableIpv6Info) {
routableIpv6Info = info;
}
}
}
if (ipv4Info && ipv6Info && routableIpv6Info && uniqueLocalIpv6Info) {
break;
}
}
// An interface listed by the platform helper can legitimately have no
// usable IPv4/IPv6 address — for instance a down virtual interface, or
// one whose only address was filtered out by `excludeIpv6`. Asserting
// here aborted the whole enumeration; skipping is the correct response.
if (!ipv4Info && !ipv6Info) {
debug("Skipping interface '%s': no usable IPv4 or IPv6 address", name);
continue;
}
if (this.excludeIpv6Only && !ipv4Info) {
continue;
}
const networkInterface = {
name: name,
loopback: internal,
mac: ((ipv4Info === null || ipv4Info === void 0 ? void 0 : ipv4Info.mac) || (ipv6Info === null || ipv6Info === void 0 ? void 0 : ipv6Info.mac)),
};
if (ipv4Info) {
networkInterface.ipv4 = ipv4Info.address;
networkInterface.ip4Netmask = ipv4Info.netmask;
networkInterface.ipv4Netaddress = (0, domain_formatter_1.getNetAddress)(ipv4Info.address, ipv4Info.netmask);
}
if (ipv6Info) {
networkInterface.ipv6 = ipv6Info.address;
networkInterface.ipv6Netmask = ipv6Info.netmask;
}
if (routableIpv6Info) {
networkInterface.globallyRoutableIpv6 = routableIpv6Info.address;
networkInterface.globallyRoutableIpv6Netmask = routableIpv6Info.netmask;
}
if (uniqueLocalIpv6Info) {
networkInterface.uniqueLocalIpv6 = uniqueLocalIpv6Info.address;
networkInterface.uniqueLocalIpv6Netmask = uniqueLocalIpv6Info.netmask;
}
interfaces.set(name, networkInterface);
}
return interfaces;
}
static resolveInterface(address) {
let interfaceName;
outer: for (const [name, infoArray] of Object.entries(os_1.default.networkInterfaces())) {
for (const info of infoArray !== null && infoArray !== void 0 ? infoArray : []) {
if (info.address === address) {
interfaceName = name;
break outer; // exit out of both loops
}
}
}
return interfaceName;
}
static async getNetworkInterfaceNames() {
// this function will always include the loopback interface
let promise;
switch (os_1.default.platform()) {
case "win32":
promise = NetworkManager.getWindowsNetworkInterfaces();
break;
case "linux": {
promise = NetworkManager.getLinuxNetworkInterfaces();
break;
}
case "darwin":
promise = NetworkManager.getDarwinNetworkInterfaces();
break;
case "freebsd": {
promise = NetworkManager.getFreeBSDNetworkInterfaces();
break;
}
case "openbsd":
case "sunos": {
promise = NetworkManager.getOpenBSD_SUNOS_NetworkInterfaces();
break;
}
default:
debug("Found unsupported platform %s", os_1.default.platform());
return Promise.reject(new Error("unsupported platform!"));
}
let names;
try {
names = await promise;
}
catch (error) {
if (error.message !== NetworkManager.NOTHING_FOUND_MESSAGE) {
throw error;
}
names = [];
}
const loopback = NetworkManager.getLoopbackInterface();
if (!names.includes(loopback)) {
names.unshift(loopback);
}
return promise;
}
static assumeNetworkInterfaceNames() {
// this method is a fallback trying to calculate network related interfaces in an platform independent way
const names = [];
Object.entries(os_1.default.networkInterfaces()).forEach(([name, infos]) => {
for (const info of infos !== null && infos !== void 0 ? infos : []) {
// we add the loopback interface or interfaces which got a unique (global or local) ipv6 address
// we currently don't just add all interfaces with ipv4 addresses as are often interfaces like VPNs, container/vms related
// unique global or unique local ipv6 addresses give an indication that we are truly connected to "the Internet"
// as something like SLAAC must be going on
// in the end
// @ts-expect-error Nodejs 18+ uses the number 4/6 instead of the string "IPv4"/"IPv6"
if (info.internal || (info.family === "IPv4" || info.family === 4) || (info.family === "IPv6" || info.family === 6) && info.scopeid === 0) {
if (!names.includes(name)) {
names.push(name);
}
break;
}
}
});
return names;
}
static getLoopbackInterface() {
for (const [name, infos] of Object.entries(os_1.default.networkInterfaces())) {
for (const info of infos !== null && infos !== void 0 ? infos : []) {
if (info.internal) {
return name;
}
}
}
throw new Error("Could not detect loopback interface!");
}
static getWindowsNetworkInterfaces() {
// does not return loopback interface
return new Promise((resolve, reject) => {
child_process_1.default.exec("arp -a | findstr /C:\"---\"", { windowsHide: true }, (error, stdout) => {
if (error) {
reject(error);
return;
}
const lines = stdout.split(os_1.default.EOL);
const addresses = [];
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i].trim().split(" ");
if (line[line.length - 3]) {
addresses.push(line[line.length - 3]);
}
else {
debug(`WINDOWS: Failed to read interface name from line ${i}: '${lines[i]}'`);
}
}
const names = [];
for (const address of addresses) {
const name = NetworkManager.resolveInterface(address);
if (name) {
if (!names.includes(name)) {
names.push(name);
}
}
else {
debug(`WINDOWS: Couldn't resolve to an interface name from '${address}'`);
}
}
if (names.length) {
resolve(names);
}
else {
reject(new Error(NetworkManager.NOTHING_FOUND_MESSAGE));
}
});
});
}
static getDarwinNetworkInterfaces() {
/*
* Previous efforts used the routing table to get all relevant network interfaces.
* Particularly using "netstat -r -f inet -n".
* First attempt was to use the "default" interface to the 0.0.0.0 catch all route using "route get 0.0.0.0".
* Though this fails when the router isn't connected to the internet, thus no "internet route" exists.
*/
// does not return loopback interface
return new Promise((resolve, reject) => {
// for ipv6 "ndp -a -n |grep -v permanent" with filtering for "expired"
child_process_1.default.exec("arp -a -n -l", { windowsHide: true }, async (error, stdout) => {
if (error) {
reject(error);
return;
}
const lines = stdout.split(os_1.default.EOL);
const names = [];
for (let i = 1; i < lines.length - 1; i++) {
const interfaceName = lines[i].trim().split(NetworkManager.SPACE_PATTERN)[4];
if (!interfaceName) {
debug(`DARWIN: Failed to read interface name from line ${i}: '${lines[i]}'`);
continue;
}
if (!names.includes(interfaceName)) {
names.push(interfaceName);
}
}
const promises = [];
for (const name of names) {
const promise = NetworkManager.getDarwinWifiNetworkState(name).then(state => {
if (state !== 1 /* WifiState.NOT_A_WIFI_INTERFACE */ && state !== 3 /* WifiState.CONNECTED */) {
// removing wifi networks which are not connected to any networks
const index = names.indexOf(name);
if (index !== -1) {
names.splice(index, 1);
}
}
});
promises.push(promise);
}
await Promise.all(promises);
if (names.length) {
resolve(names);
}
else {
reject(new Error(NetworkManager.NOTHING_FOUND_MESSAGE));
}
});
});
}
static getLinuxNetworkInterfaces() {
// does not return loopback interface
return new Promise((resolve, reject) => {
// "ip -o link show" lists all network interfaces (one per line) without exposing
// neighbor/ARP table data about other machines on the network.
// The -o flag ensures one-line-per-interface output for reliable parsing.
child_process_1.default.exec("ip -o link show", { windowsHide: true }, (error, stdout) => {
if (error) {
if (error.message.includes("ip: not found")) {
debug("LINUX: ip was not found on the system. Falling back to assuming network interfaces!");
resolve(NetworkManager.assumeNetworkInterfaceNames());
return;
}
reject(error);
return;
}
const lines = stdout.split(os_1.default.EOL);
const names = [];
for (let i = 0; i < lines.length - 1; i++) {
const parts = lines[i].trim().split(NetworkManager.SPACE_PATTERN);
// ip -o link show output format: "<index>: <name>: <flags> ..."
// need at least 3 parts: index, name, flags
if (parts.length < 3 || !parts[1] || !parts[2]) {
debug(`LINUX: Failed to parse interface from line ${i}: '${lines[i]}'`);
continue;
}
// parts[1] is "<name>:" — strip the trailing colon
const interfaceName = parts[1].replace(/:$/, "");
if (!interfaceName) {
debug(`LINUX: Failed to read interface name from line ${i}: '${lines[i]}'`);
continue;
}
// parts[2] contains the interface flags e.g. "<BROADCAST,MULTICAST,UP,LOWER_UP>"
// skip loopback interfaces
if (parts[2].includes("LOOPBACK")) {
continue;
}
if (!names.includes(interfaceName)) {
names.push(interfaceName);
}
}
if (names.length) {
resolve(names);
}
else {
reject(new Error(NetworkManager.NOTHING_FOUND_MESSAGE));
}
});
});
}
static getFreeBSDNetworkInterfaces() {
// does not return loopback interface
return new Promise((resolve, reject) => {
child_process_1.default.exec("arp -a -n", { windowsHide: true }, (error, stdout) => {
if (error) {
reject(error);
return;
}
const lines = stdout.split(os_1.default.EOL);
const names = [];
for (let i = 0; i < lines.length - 1; i++) {
const interfaceName = lines[i].trim().split(NetworkManager.SPACE_PATTERN)[5];
if (!interfaceName) {
debug(`FreeBSD: Failed to read interface name from line ${i}: '${lines[i]}'`);
continue;
}
if (!names.includes(interfaceName)) {
names.push(interfaceName);
}
}
if (names.length) {
resolve(names);
}
else {
reject(new Error(NetworkManager.NOTHING_FOUND_MESSAGE));
}
});
});
}
static getOpenBSD_SUNOS_NetworkInterfaces() {
// does not return loopback interface
return new Promise((resolve, reject) => {
// for ipv6 something like "ndp -a -n | grep R" (grep for reachable; maybe exclude permanent?)
child_process_1.default.exec("arp -a -n", { windowsHide: true }, (error, stdout) => {
if (error) {
reject(error);
return;
}
const interfaceArrayOffset = os_1.default.platform() === "sunos" ? 0 : 2;
const lines = stdout.split(os_1.default.EOL);
const names = [];
for (let i = 1; i < lines.length - 1; i++) {
const interfaceName = lines[i].trim().split(NetworkManager.SPACE_PATTERN)[interfaceArrayOffset];
if (!interfaceName) {
debug(`${os_1.default.platform()}: Failed to read interface name from line ${i}: '${lines[i]}'`);
continue;
}
if (!names.includes(interfaceName)) {
names.push(interfaceName);
}
}
if (names.length) {
resolve(names);
}
else {
reject(new Error(NetworkManager.NOTHING_FOUND_MESSAGE));
}
});
});
}
static getDarwinWifiNetworkState(name) {
return new Promise(resolve => {
/*
* networksetup outputs the following in the listed scenarios:
*
* executed for an interface which is not a Wi-Fi interface:
* "<name> is not a Wi-Fi interface.
* Error: Error obtaining wireless information."
*
* executed for a turned off Wi-Fi interface:
* "You are not associated with an AirPort network.
* Wi-Fi power is currently off."
*
* executed for a turned on Wi-Fi interface which is not connected:
* "You are not associated with an AirPort network."
*
* executed for a connected Wi-Fi interface:
* "Current Wi-Fi Network: <network name>"
*
* Other messages handled here.
* "All Wi-Fi network services are disabled": encountered on macOS VM machines
*/
child_process_1.default.exec("networksetup -getairportnetwork " + name, { windowsHide: true }, (error, stdout) => {
if (error) {
if (stdout.includes("not a Wi-Fi interface")) {
resolve(1 /* WifiState.NOT_A_WIFI_INTERFACE */);
return;
}
console.log(`CIAO WARN: While checking networksetup for ${name} encountered an error (${error.message}) with output: ${stdout.replace(os_1.default.EOL, "; ")}`);
resolve(0 /* WifiState.UNDEFINED */);
return;
}
let wifiState = 0 /* WifiState.UNDEFINED */;
if (stdout.includes("not a Wi-Fi interface")) {
wifiState = 1 /* WifiState.NOT_A_WIFI_INTERFACE */;
}
else if (stdout.includes("Current Wi-Fi Network")) {
wifiState = 3 /* WifiState.CONNECTED */;
}
else if (stdout.includes("not associated")) {
wifiState = 2 /* WifiState.NOT_ASSOCIATED */;
}
else if (stdout.includes("All Wi-Fi network services are disabled")) {
// typically encountered on a macOS VM or something not having a WiFi card
wifiState = 1 /* WifiState.NOT_A_WIFI_INTERFACE */;
}
else {
console.log(`CIAO WARN: While checking networksetup for ${name} encountered an unknown output: ${stdout.replace(os_1.default.EOL, "; ")}`);
}
resolve(wifiState);
});
});
}
}
exports.NetworkManager = NetworkManager;
NetworkManager.SPACE_PATTERN = /\s+/g;
NetworkManager.NOTHING_FOUND_MESSAGE = "no interfaces found";
NetworkManager.POLLING_TIME = 15 * 1000; // 15 seconds
//# sourceMappingURL=NetworkManager.js.map