UNPKG

netgear

Version:

Node module to interact with Netgear routers via SOAP

1,369 lines (1,291 loc) 86.4 kB
/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. Copyright 2017 - 2023, Robin de Gruijter <gruijter@hotmail.com> */ /* eslint-disable no-await-in-loop */ /* eslint-disable prefer-destructuring */ /* eslint-disable max-classes-per-file */ 'use strict'; // process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; const http = require('http'); const https = require('https'); const parseXml = require('xml-js'); const Queue = require('smart-request-balancer'); const util = require('util'); const dns = require('dns'); const dgram = require('dgram'); // const net = require('net'); // const Buffer = require('buffer').Buffer; const os = require('os'); const soap = require('./soapcalls'); // const { hasUncaughtExceptionCaptureCallback } = require('process'); const setTimeoutPromise = util.promisify(setTimeout); const dnsLookupPromise = util.promisify(dns.lookup); const regexResponseCode = /<ResponseCode>(.*)<\/ResponseCode>/; const regexAttachedDevices = /<NewAttachDevice>(.*)<\/NewAttachDevice>/s; const regexAllowedDevices = /<NewAllowDeviceList>(.*)<\/NewAllowDeviceList>/s; const regexNewTodayUpload = /<NewTodayUpload>(.*)<\/NewTodayUpload>/; const regexNewTodayDownload = /<NewTodayDownload>(.*)<\/NewTodayDownload>/; const regexNewMonthUpload = /<NewMonthUpload>(.*)<\/NewMonthUpload>/; const regexNewMonthDownload = /<NewMonthDownload>(.*)<\/NewMonthDownload>/; const regexCurrentVersion = /<CurrentVersion>(.*)<\/CurrentVersion>/; const regexNewVersion = /<NewVersion>(.*)<\/NewVersion>/; const regexReleaseNote = /<ReleaseNote>(.*)<\/ReleaseNote>/s; const regexNewUplinkBandwidth = /<NewUplinkBandwidth>(.*)<\/NewUplinkBandwidth>/; const regexNewDownlinkBandwidth = /<NewDownlinkBandwidth>(.*)<\/NewDownlinkBandwidth>/; const regexCurrentDeviceBandwidth = /<NewCurrentDeviceBandwidth>(.*)<\/NewCurrentDeviceBandwidth>/; const regexCurrentDeviceUpBandwidth = /<NewCurrentDeviceUpBandwidth>(.*)<\/NewCurrentDeviceUpBandwidth>/; const regexCurrentDeviceDownBandwidth = /<NewCurrentDeviceDownBandwidth>(.*)<\/NewCurrentDeviceDownBandwidth>/; const regexNewSettingMethod = /<NewSettingMethod>(.*)<\/NewSettingMethod>/; const regexUplinkBandwidth = /<NewOOKLAUplinkBandwidth>(.*)<\/NewOOKLAUplinkBandwidth>/; const regexDownlinkBandwidth = /<NewOOKLADownlinkBandwidth>(.*)<\/NewOOKLADownlinkBandwidth>/; const regexAveragePing = /<AveragePing>(.*)<\/AveragePing>/; const regexParentalControl = /<ParentalControl>(.*)<\/ParentalControl>/; const regexNewQoSEnableStatus = /<NewQoSEnableStatus>(.*)<\/NewQoSEnableStatus>/; const regexNewSmartConnectEnable = /<NewSmartConnectEnable>(.*)<\/NewSmartConnectEnable>/; const regexNewBlockDeviceEnable = /<NewBlockDeviceEnable>(.*)<\/NewBlockDeviceEnable>/; const regexNewTrafficMeterEnable = /<NewTrafficMeterEnable>(.*)<\/NewTrafficMeterEnable>/; const regexNewControlOption = /<NewControlOption>(.*)<\/NewControlOption>/; const regexNewMonthlyLimit = /<NewMonthlyLimit>(.*)<\/NewMonthlyLimit>/; const regexRestartHour = /<RestartHour>(.*)<\/RestartHour>/; const regexRestartMinute = /<RestartMinute>(.*)<\/RestartMinute>/; const regexRestartDay = /<RestartDay>(.*)<\/RestartDay>/; const regexNewAvailableChannel = /<NewAvailableChannel>(.*)<\/NewAvailableChannel>/; const regexNewChannel = /<NewChannel>(.*)<\/NewChannel>/; const regexNew5GChannel = /<New5GChannel>(.*)<\/New5GChannel>/; const regexNew5G1Channel = /<New5G1Channel>(.*)<\/New5G1Channel>/; const regexNewLogDetails = /<NewLogDetails>(.*)<\/NewLogDetails>/s; const regexSysUpTime = /<SysUpTime>(.*)<\/SysUpTime>/; const regexNewEthernetLinkStatus = /<NewEthernetLinkStatus>(.*)<\/NewEthernetLinkStatus>/; const defaultHost = 'routerlogin.net'; const defaultUser = 'admin'; const defaultPassword = 'password'; // const defaultPort = 5000; // 80 for orbi and R7800 const defaultSessionId = 'A7D88AE69687E58D9A00'; // '10588AE69687E58D9A00' class AttachedDevice { constructor() { this.IP = undefined; // e.g. '10.0.0.10' this.Name = undefined; // '--' for unknown this.NameUserSet = undefined; // e.g. 'false' this.MAC = undefined; // e.g. '61:56:FA:1B:E1:21' this.ConnectionType = undefined; // e.g. 'wired', '2.4GHz', 'Guest Wireless 2.4G' this.SSID = undefined; // e.g. 'MyWiFi' this.Linkspeed = undefined; this.SignalStrength = undefined; // number <= 100 this.AllowOrBlock = undefined; // 'Allow' or 'Block' this.Schedule = undefined; // e.g. 'false' this.DeviceType = undefined; // a number this.DeviceTypeV2 = undefined; // e.g. 1, found in R7000 response this.DeviceTypeUserSet = undefined; // e.g. 'false', this.DeviceTypeName = undefined; // unknown, found in orbi response this.DeviceTypeNameV2 = undefined; // 'Computer (Generic)', found in R7000 response this.DeviceModel = undefined; // unknown, found in R7800 and orbi response this.DeviceModelUserSet = undefined; // boolean , found in orbi response this.Upload = undefined; // e.g. 0 this.Download = undefined; // e.g. 0 this.QosPriority = undefined; // 1, 2, 3, 4 this.Grouping = undefined; // e.g. 0 this.SchedulePeriod = undefined; this.ConnAPMAC = undefined; // unknown, found in orbi response } } // filters for illegal xml characters // XML 1.0 // #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] const xml10pattern = '[^' + '\u0009\r\n' + '\u0020-\uD7FF' + '\uE000-\uFFFD' + '\ud800\udc00-\udbff\udfff' + ']'; // // XML 1.1 // // [#x1-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] // const xml11pattern = '[^' // + '\u0001-\uD7FF' // + '\uE000-\uFFFD' // + '\ud800\udc00-\udbff\udfff' // + ']+'; const patchBody = (body) => body .replace(xml10pattern, '') .replace(/soap-env:envelope/gi, 'v:Envelope') .replace(/soap-env:body/gi, 'v:Body'); class NetgearRouter { // password, username, host and port are deprecated. Now use { password: '', username: '', host:'routerlogin.net', port: 80, timeout: 19000, tls: false} constructor(opts, username, host, port) { const options = opts || {}; this.host = options.host || host || defaultHost; this.port = options.port || port; // defaults with tls: 443, 5555. no tls: 5000, 80 this.tls = options.tls === undefined ? (this.port !== 80) : options.tls; // set tls true as default, except when using port 80 this.username = options.username || username || defaultUser; this.password = options.password || opts || defaultPassword; this.timeout = options.timeout || 18000; this.sessionId = defaultSessionId; this.cookie = undefined; this.loggedIn = false; this.configStarted = false; this.soapVersion = undefined; // 2 or 3 this.loginMethod = undefined; // 2 for newer models, 1 for old models this.getAttachedDevicesMethod = undefined; // 2 or 1 this.checkNewFirmwareMethod = undefined; // 2 or 1 this.guestWifiMethod = { set24_1: undefined, get50_1: undefined, set50_1: undefined, }; this.lastResponse = undefined; this._initQueue(); } /** * Discovers a netgear router in the network. Also sets the discovered ip address and soap port for this session. * @param {dnsLookupOptions} [options] - dnsLookup options, e.g. { family: 4 } * @returns {Promise.<currentSetting>} The discovered router info, including host ip address and soap port. */ async discover(dnsLookupOptions) { try { const discoveredInfo = await this._discoverHostInfo(dnsLookupOptions); this.host = discoveredInfo.host; this.port = discoveredInfo.port; this.tls = discoveredInfo.tls; return Promise.resolve(discoveredInfo); } catch (error) { return Promise.reject(error); } } /** * Login to the router. Passing options will override any existing session settings. * If host or port are not set, login will try to auto discover these. * @param {sessionOptions} [options] - configurable session options * @returns {Promise.<loggedIn>} The loggedIn state. */ async login(opts, username, host, port) { try { const options = opts || {}; if (typeof opts === 'string') { this.password = opts; } else { this.password = options.password || this.password; } this.host = options.host || host || await this.host; this.port = options.port || port || await this.port; this.tls = options.tls === undefined ? await this.tls : options.tls; this.username = options.username || username || this.username; this.timeout = options.timeout || this.timeout; if (!this.host || this.host === '') { await this.discover() .catch(() => { throw Error('Cannot login: host IP and/or SOAP port not set'); }); } // discover soap port, tls and login method supported by router if (!this.loginMethod || !this.port) { const currentSetting = await this.getCurrentSetting(); // will set this.loginMethod if (!this.port) this.port = currentSetting.port; // keep manually set port if (this.tls === undefined) this.tls = currentSetting.tls; // keep manual set tls } let loggedIn = false; const messageNew = soap.login(this.sessionId, this.username, this.password); const messageOld = soap.loginOld(this.sessionId, this.username, this.password); // use old method if opts method 1 selected, or auto method selected and loginMethod < 2 if (options.method === 1 || (!options.method && this.loginMethod < 2)) { this.cookie = undefined; // reset the cookie loggedIn = await this._queueMessage(soap.action.loginOld, messageOld) .then(() => true) .catch(() => false); } // use new method if opts method 2 selected, or auto method selected and loginMethod = 2 if (options.method === 2 || (!options.method && this.loginMethod >= 2)) { loggedIn = await this._queueMessage(soap.action.login, messageNew) .then(() => true) .catch(() => { this.cookie = undefined; // reset the cookie return false; }); } // use old login method as first fallback, only if auto method selected if (!options.method && !loggedIn) { loggedIn = await this._queueMessage(soap.action.loginOld, messageOld) .then(() => true) .catch(() => false); } // use new login method as second fallback, only if auto method selected if (!options.method && !loggedIn) { loggedIn = await this._queueMessage(soap.action.login, messageNew) .catch(() => { this.cookie = undefined; // reset the cookie return false; }); } if (!loggedIn) throw Error('Failed to login'); this.loggedIn = true; return Promise.resolve(this.loggedIn); } catch (error) { return Promise.reject(error); } } /** * Logout from the router. * @returns {Promise.<loggedIn>} The loggedIn state. */ async logout() { try { const message = soap.logout(this.sessionId); await this._queueMessage(soap.action.logout, message); this.loggedIn = false; return Promise.resolve(this.loggedIn); } catch (error) { return Promise.reject(error); } } /** * Get router information without need for credentials. Autodiscovers the SOAP port and TLS * @param {string} [host] - The url or ip address of the router. * @returns {Promise.<currentSetting>} */ async getCurrentSetting(host, timeout) { try { const host1 = host || this.host; const headers = { }; const options = { hostname: host1, port: 80, path: '/currentsetting.htm', headers, method: 'GET', }; const result = await this._makeHttpRequest(options, '', timeout); this.lastResponse = result.body; // request failed if (result.statusCode !== 200) { throw Error(`HTTP request Failed. Status Code: ${result.statusCode}`); } if (!result.body.includes('Model=')) { throw Error('This is not a valid Netgear router'); } // request successfull const currentSetting = {}; const entries = result.body.split(/[\r\n\t\s]+/gm); Object.keys(entries).forEach((entry) => { const info = entries[entry].split('='); if (info.length === 2) { currentSetting[info[0]] = info[1]; } }); currentSetting.host = host1; // add the host address to the information currentSetting.port = currentSetting.SOAP_HTTPs_Port || await this._getSoapPort(host1); // add port address to the information currentSetting.tls = !!currentSetting.SOAP_HTTPs_Port; if (currentSetting.port === 443 || currentSetting.port === 5555 || currentSetting.port === 5043) currentSetting.tls = true; // add tls to the information this.loginMethod = Number(currentSetting.LoginMethod) || 1; this.soapVersion = parseInt(currentSetting.SOAPVersion, 10) || 2; return Promise.resolve(currentSetting); } catch (error) { return Promise.reject(error); } } /** * Get system Info. * @returns {Promise.<systemInfo>} */ async getSystemInfo() { try { const message = soap.getSystemInfo(this.sessionId); const result = await this._queueMessage(soap.action.getSystemInfo, message); // parse xml to json object const parseOptions = { compact: true, nativeType: true, ignoreDeclaration: true, ignoreAttributes: true, spaces: 2, }; const rawJson = parseXml.xml2js(result.body, parseOptions); const entries = rawJson['v:Envelope']['v:Body']['m:GetSystemInfoResponse']; const systemInfo = {}; Object.keys(entries).forEach((property) => { if (Object.prototype.hasOwnProperty.call(entries, property)) { systemInfo[property] = entries[property]._text; } }); return Promise.resolve(systemInfo); } catch (error) { return Promise.reject(error); } } /** * Get router logs. * @param {boolean} [parse = false] - will parse the results to json if true * @returns {Promise.<logs>} */ async getSystemLogs(parse) { try { const message = soap.getSystemLogs(this.sessionId); const result = await this._queueMessage(soap.action.getSystemLogs, message); if (!result.body.includes('</NewLogDetails>')) throw Error('Incorrect or incomplete response from router'); const raw = regexNewLogDetails.exec(result.body)[1]; const entries = raw .split(/[\r\n]+/gm) .filter((entry) => entry.length > 0); if (entries.length < 1) { throw Error('No log entries found'); } if (!parse) { return Promise.resolve(entries); } // start parsing stuff const logs = entries .map((entry) => { const items = entry.split(','); return { string: entry, event: `${entry.split(']')[0]}`.replace(/[[\]]/g, ''), info: items[0].split(']')[1] ? items[0].split(']')[1].trim() : undefined, ts: Date.parse(`${items[items.length - 2]}, ${items[items.length - 1]}`), }; }); return Promise.all(logs); } catch (error) { return Promise.reject(error); } } /** * Get router uptime since last boot. * @returns {Promise.<hh:mm:ss>} */ async getSysUpTime() { try { const message = soap.getSysUpTime(this.sessionId); const result = await this._queueMessage(soap.action.getSysUpTime, message); if (!result.body.includes('</SysUpTime>')) throw Error('Incorrect or incomplete response from router'); const sysUpTime = regexSysUpTime.exec(result.body)[1]; return Promise.resolve(sysUpTime); } catch (error) { return Promise.reject(error); } } /** * @typedef NTPservers * @description TimeZoneInfo is an object with these properties. * @property {string} NTPServer1 e.g. 'time-g.netgear.com' * @property {string} NTPServer2 e.g. 'time-g.netgear.com' * @property {string} NTPServer3 e.g. 'time-g.netgear.com' * @property {string} NTPServer4 e.g. 'time-g.netgear.com' * @example // NTPservers { NTPServer1: 'time-g.netgear.com', NTPServer2: 'time-g.netgear.com', NTPServer3: 'time-g.netgear.com', NTPServer4: 'time-g.netgear.com' } */ /** * Get NTP servers. * @returns {Promise.<NTPservers>} */ async getNTPServers() { try { const message = soap.getNTPServers(this.sessionId); const result = await this._queueMessage(soap.action.getNTPServers, message); if (!result.body.includes('m:GetInfoResponse')) throw Error('Incorrect or incomplete response from router'); // parse xml to json object const parseOptions = { compact: true, ignoreDeclaration: true, ignoreAttributes: true, spaces: 2, }; const rawJson = parseXml.xml2js(result.body, parseOptions); const entries = rawJson['v:Envelope']['v:Body']['m:GetInfoResponse']; const info = {}; Object.keys(entries).forEach((property) => { if (Object.prototype.hasOwnProperty.call(entries, property)) { const propname = property.replace('New', ''); info[propname] = entries[property]._text; } }); return Promise.resolve(info); } catch (error) { return Promise.reject(error); } } /** * @typedef timeZoneInfo * @description TimeZoneInfo is an object with these properties. * @property {string} TimeZone e.g. '+1' * @property {string} DaylightSaving e.g. '0' * @property {string} IndexValue e.g. '19' * @example // timeZoneInfo { TimeZone: '+1', DaylightSaving: '0', IndexValue: '19' } */ /** * Get TimeZone. * @returns {Promise.<timeZoneInfo>} */ async getTimeZoneInfo() { try { const message = soap.getTimeZoneInfo(this.sessionId); const result = await this._queueMessage(soap.action.getTimeZoneInfo, message); if (!result.body.includes('m:GetTimeZoneInfoResponse')) throw Error('Incorrect or incomplete response from router'); // parse xml to json object const parseOptions = { compact: true, ignoreDeclaration: true, ignoreAttributes: true, spaces: 2, }; const rawJson = parseXml.xml2js(result.body, parseOptions); const entries = rawJson['v:Envelope']['v:Body']['m:GetTimeZoneInfoResponse']; const info = {}; Object.keys(entries).forEach((property) => { if (Object.prototype.hasOwnProperty.call(entries, property)) { const propname = property.replace('New', ''); // propname = propname.charAt(0).toLowerCase() + propname.slice(1); info[propname] = entries[property]._text; } }); // const regExTimeZone = /<NewTimeZone>(.*)<\/NewTimeZone>/; // const regExDaylightSaving = /<NewDaylightSaving>(.*)<\/NewDaylightSaving>/; // const regExIndexValue = /<NewIndexValue>(.*)<\/NewIndexValue>/; // const info = { // timeZone: result.body.match(regExTimeZone)[1], // daylightSaving: result.body.match(regExDaylightSaving)[1], // indexValue: result.body.match(regExIndexValue)[1], // }; return Promise.resolve(info); } catch (error) { return Promise.reject(error); } } /** * Get router information. * @returns {Promise.<info>} */ async getInfo() { try { const message = soap.getInfo(this.sessionId); const result = await this._queueMessage(soap.action.getInfo, message); // const patchedBody = patchBody(result.body); // parse xml to json object const parseOptions = { compact: true, ignoreDeclaration: true, ignoreAttributes: true, spaces: 2, }; const rawJson = parseXml.xml2js(result.body, parseOptions); const entries = rawJson['v:Envelope']['v:Body']['m:GetInfoResponse']; const info = {}; Object.keys(entries).forEach((property) => { if (Object.prototype.hasOwnProperty.call(entries, property)) { const propname = property.replace('New', ''); info[propname] = entries[property]._text; } }); return Promise.resolve(info); } catch (error) { return Promise.reject(error); } } /** * Get router SupportFeatureList. * @returns {Promise.<supportFeatureList>} */ async getSupportFeatureListXML() { try { const message = soap.getSupportFeatureListXML(this.sessionId); const result = await this._queueMessage(soap.action.getSupportFeatureListXML, message); // const patchedBody = patchBody(result.body); // parse xml to json object const parseOptions = { compact: true, ignoreDeclaration: true, ignoreAttributes: true, spaces: 2, }; const rawJson = parseXml.xml2js(result.body, parseOptions); const entries = rawJson['v:Envelope']['v:Body']['m:GetSupportFeatureListXMLResponse'].newFeatureList.features; const supportFeatureList = {}; Object.keys(entries).forEach((property) => { if (Object.prototype.hasOwnProperty.call(entries, property)) { supportFeatureList[property] = entries[property]._text; } }); return Promise.resolve(supportFeatureList); } catch (error) { return Promise.reject(error); } } /** * Get Device Config. * @returns {Promise.<deviceConfig>} */ async getDeviceConfig() { try { const message = soap.getDeviceConfig(this.sessionId); const result = await this._queueMessage(soap.action.getDeviceConfig, message); // const patchedBody = patchBody(result.body); // parse xml to json object const parseOptions = { compact: true, ignoreDeclaration: true, ignoreAttributes: true, spaces: 2, }; const rawJson = parseXml.xml2js(result.body, parseOptions); const entries = rawJson['v:Envelope']['v:Body']['m:GetInfoResponse']; const deviceConfig = {}; Object.keys(entries).forEach((property) => { if (Object.prototype.hasOwnProperty.call(entries, property)) { deviceConfig[property] = entries[property]._text; } }); return Promise.resolve(deviceConfig); } catch (error) { return Promise.reject(error); } } /** * Get Allowed Device list. * @returns {Promise.<allowedDevice[]>} */ async getDeviceListAll() { try { const message = soap.getDeviceListAll(this.sessionId); const result = await this._queueMessage(soap.action.getDeviceListAll, message); const devices = []; const body = result.body .replace(/&amp;/gi, '&') .replace(/&lt;/gi, '<') .replace(/&gt;/gi, '>'); const raw = regexAllowedDevices.exec(body)[1]; const entries = raw.split('@'); entries.forEach((entry, index) => { const info = entry.split(';'); // info must be larger then 0 chars if (info.length === 0) { throw Error('Error parsing device-list'); } // check if first entry is number of entries if (index === 0 && info.length === 1) { if (Number(entry) !== entries.length - 1) throw Error('Error parsing device-list - number mismatch'); return; } // error when not enough info elements if (info.length < 4) throw Error('Error parsing device-list - not enough elements'); // throw error on invalid mac format if (info[1].length !== 17) throw Error('Error parsing device-list - invalid mac format'); const device = { MAC: info[1], // e.g. '61:56:FA:1B:E1:21' Name: info[2], // '--' for unknown ConnectionType: info[3], // 'wired' or 'wireless' }; devices.push(device); }); return Promise.all(devices); } catch (error) { return Promise.reject(error); } } /** * Get LAN config * @returns {Promise.<LANConfig>} */ async getLANConfig() { try { const message = soap.getLANConfig(this.sessionId); const result = await this._queueMessage(soap.action.getLANConfig, message); // const patchedBody = patchBody(result.body); // parse xml to json object const parseOptions = { compact: true, ignoreDeclaration: true, ignoreAttributes: true, spaces: 2, }; const rawJson = parseXml.xml2js(result.body, parseOptions); const entries = rawJson['v:Envelope']['v:Body']['m:GetInfoResponse']; const LANConfig = {}; Object.keys(entries).forEach((property) => { if (Object.prototype.hasOwnProperty.call(entries, property)) { LANConfig[property] = entries[property]._text; } }); return Promise.resolve(LANConfig); } catch (error) { return Promise.reject(error); } } /** * Get Internet connection status, e.g. 'Up' * @returns {Promise.<ethernetLinkStatus>} */ async getEthernetLinkStatus() { try { const message = soap.getEthernetLinkStatus(this.sessionId); const result = await this._queueMessage(soap.action.getEthernetLinkStatus, message); if (!result.body.includes('</NewEthernetLinkStatus>')) throw Error('Incorrect or incomplete response from router'); const ethernetLinkStatus = regexNewEthernetLinkStatus.exec(result.body)[1]; return Promise.resolve(ethernetLinkStatus); } catch (error) { return Promise.reject(error); } } /** * Get WAN config * @returns {Promise.<WANConfig>} */ async getWANConfig() { try { const message = soap.getWANIPConnection(this.sessionId); const result = await this._queueMessage(soap.action.getWANIPConnection, message); // const patchedBody = patchBody(result.body); // parse xml to json object const parseOptions = { compact: true, ignoreDeclaration: true, ignoreAttributes: true, spaces: 2, }; const rawJson = parseXml.xml2js(result.body, parseOptions); const entries = rawJson['v:Envelope']['v:Body']['m:GetInfoResponse']; const WANConfig = {}; Object.keys(entries).forEach((property) => { if (Object.prototype.hasOwnProperty.call(entries, property)) { WANConfig[property] = entries[property]._text; } }); return Promise.resolve(WANConfig); } catch (error) { return Promise.reject(error); } } /** * @typedef WANConnectionType * @description WANConnectionType is an object with these properties. * @property {string} ConnectionType e.g. 'DHCP' * @example // WANConnectionType { ConnectionType: 'DHCP' } */ /** * Get WAN Connection Type * @returns {Promise.<WANConnectionType>} */ async getWANConnectionType() { try { const message = soap.getWANConnectionTypeInfo(this.sessionId); const result = await this._queueMessage(soap.action.getWANConnectionTypeInfo, message); if (!result.body.includes('m:GetConnectionTypeInfoResponse')) throw Error('Incorrect or incomplete response from router'); // parse xml to json object const parseOptions = { compact: true, ignoreDeclaration: true, ignoreAttributes: true, spaces: 2, }; const rawJson = parseXml.xml2js(result.body, parseOptions); const entries = rawJson['v:Envelope']['v:Body']['m:GetConnectionTypeInfoResponse']; const info = {}; Object.keys(entries).forEach((property) => { if (Object.prototype.hasOwnProperty.call(entries, property)) { const propname = property.replace('New', ''); // propname = propname.charAt(0).toLowerCase() + propname.slice(1); info[propname] = entries[property]._text; } }); return Promise.resolve(info); } catch (error) { return Promise.reject(error); } } /** * @typedef WANInternetPort * @description WANInternetPort is an object with these properties. * @property {string} InternetPortInfo e.g. '1@1;Ethernet' * @example // WANInternetPort { InternetPortInfo: '1@1;Ethernet' } */ /** * Get WAN Internet Port Info * @returns {Promise.<WANInternetPort>} */ async getWANInternetPort() { try { const message = soap.getWANInternetPortInfo(this.sessionId); const result = await this._queueMessage(soap.action.getWANInternetPortInfo, message); if (!result.body.includes('m:GetInternetPortInfoResponse')) throw Error('Incorrect or incomplete response from router'); // parse xml to json object const parseOptions = { compact: true, ignoreDeclaration: true, ignoreAttributes: true, spaces: 2, }; const rawJson = parseXml.xml2js(result.body, parseOptions); const entries = rawJson['v:Envelope']['v:Body']['m:GetInternetPortInfoResponse']; const info = {}; Object.keys(entries).forEach((property) => { if (Object.prototype.hasOwnProperty.call(entries, property)) { const propname = property.replace('New', ''); // propname = propname.charAt(0).toLowerCase() + propname.slice(1); info[propname] = entries[property]._text; } }); return Promise.resolve(info); } catch (error) { return Promise.reject(error); } } /** * Get WAN DNS LookUpStatus * @returns {Promise.<WANDNSLookUpStatus>} */ async getWANDNSLookUpStatus() { try { const message = soap.getWANDNSLookUpStatus(this.sessionId); const result = await this._queueMessage(soap.action.getWANDNSLookUpStatus, message); if (!result.body.includes('m:GetDNSLookUpStatusResponse')) throw Error('Incorrect or incomplete response from router'); // parse xml to json object const parseOptions = { compact: true, ignoreDeclaration: true, ignoreAttributes: true, spaces: 2, }; const rawJson = parseXml.xml2js(result.body, parseOptions); const entries = rawJson['v:Envelope']['v:Body']['m:GetInternetPortInfoResponse']; const info = {}; Object.keys(entries).forEach((property) => { if (Object.prototype.hasOwnProperty.call(entries, property)) { const propname = property.replace('New', ''); // propname = propname.charAt(0).toLowerCase() + propname.slice(1); info[propname] = entries[property]._text; } }); return Promise.resolve(info); } catch (error) { return Promise.reject(error); } } /** * Get Port Mapping Info * @returns {Promise.<portMapping>} */ async getPortMappingInfo() { try { const message = soap.getPortMappingInfo(this.sessionId); const result = await this._queueMessage(soap.action.getPortMappingInfo, message); const patchedBody = patchBody(result.body); // parse xml to json object const parseOptions = { compact: true, ignoreDeclaration: true, ignoreAttributes: true, spaces: 2, }; const rawJson = parseXml.xml2js(patchedBody, parseOptions); const entries = rawJson['v:Envelope']['v:Body']['m:GetPortMappingInfoResponse']; const portMapping = {}; Object.keys(entries).forEach((property) => { if (Object.prototype.hasOwnProperty.call(entries, property)) { portMapping[property] = entries[property]._text; } }); return Promise.resolve(portMapping); } catch (error) { return Promise.reject(error); } } /** * Get array of attached devices. * @param {number} [method = 0] - 0: auto, 1: v1 (old), 2: v2 (new) * @returns {Promise.<AttachedDevice[]>} */ async getAttachedDevices(method) { try { let devices; this.getAttachedDevicesMethod = method; if (method === 1) { devices = await this._getAttachedDevices(); } else if (method === 2) { devices = await this._getAttachedDevices2(); } else { this.getAttachedDevicesMethod = 0; devices = await this._getAttachedDevices() .catch(() => this._getAttachedDevices2()); } return Promise.all(devices); } catch (error) { return Promise.reject(error); } } /** * Get traffic meter statistics. * @returns {Promise.<trafficStatistics>} */ async getTrafficMeter() { try { const message = soap.trafficMeter(this.sessionId); const result = await this._queueMessage(soap.action.getTrafficMeter, message); const newTodayUpload = Number(regexNewTodayUpload.exec(result.body)[1].replace(',', '')); const newTodayDownload = Number(regexNewTodayDownload.exec(result.body)[1].replace(',', '')); const newMonthUpload = Number(regexNewMonthUpload.exec(result.body)[1].split('/')[0].replace(',', '')); const newMonthDownload = Number(regexNewMonthDownload.exec(result.body)[1].split('/')[0].replace(',', '')); const trafficStatistics = { newTodayUpload, newTodayDownload, newMonthUpload, newMonthDownload, }; return Promise.resolve(trafficStatistics); } catch (error) { return Promise.reject(error); } } /** * Get Parental Control Enable Status (true / false). * @returns {Promise.<parentalControlEnabled>} */ async getParentalControlEnableStatus() { try { await this._configurationStarted(); const message = soap.getParentalControlEnableStatus(this.sessionId); const result = await this._queueMessage(soap.action.getParentalControlEnableStatus, message); await this._configurationFinished() .catch(() => { // console.log(`finished with warning`); }); const parentalControlEnabled = regexParentalControl.exec(result.body)[1] === '1'; return Promise.resolve(parentalControlEnabled); } catch (error) { return Promise.reject(error); } } /** * Set the device name * @param {string} name - e.g. 'MyNetgearRouter' * @returns {Promise<finished>} */ async setNetgearDeviceName(name) { try { const lanConfig = await this.getLANConfig(); const MAC = lanConfig.NewLANMACAddress; const message = soap.setNetgearDeviceName(this.sessionId, MAC, name); await this._queueMessage(soap.action.setNetgearDeviceName, message); return Promise.resolve(true); } catch (error) { return Promise.reject(error); } } /** * Enable or Disable Parental Controls * @param {boolean} enable - true to enable, false to disable. * @returns {Promise<finished>} */ async enableParentalControl(enable) { try { await this._configurationStarted(); const message = soap.enableParentalControl(this.sessionId, enable); await this._queueMessage(soap.action.enableParentalControl, message); await this._configurationFinished() .catch(() => { // console.log(`finished with warning`); }); return Promise.resolve(true); } catch (error) { return Promise.reject(error); } } /** * Get QoS Enable Status (true / false). * @returns {Promise.<qosEnabled>} */ async getQoSEnableStatus() { try { const message = soap.getQoSEnableStatus(this.sessionId); const result = await this._queueMessage(soap.action.getQoSEnableStatus, message); const qosEnabled = regexNewQoSEnableStatus.exec(result.body)[1] === '1'; return Promise.resolve(qosEnabled); } catch (error) { return Promise.reject(error); } } /** * Get QOS Device bandwith. Only works on R7000 * @returns {Promise.<currentDeviceBandwidth>} */ async getCurrentDeviceBandwidth() { try { const message = soap.getCurrentDeviceBandwidth(this.sessionId); const result = await this._queueMessage(soap.action.getCurrentDeviceBandwidth, message); const currentDeviceBandwidth = regexCurrentDeviceBandwidth.exec(result.body)[1]; // response: // <m:GetCurrentDeviceBandwidthResponse xmlns:m="urn:NETGEAR-ROUTER:service:AdvancedQoS:1"> // <NewCurrentDeviceBandwidth>0</NewCurrentDeviceBandwidth> // </m:GetCurrentDeviceBandwidthResponse> return Promise.resolve(currentDeviceBandwidth); } catch (error) { return Promise.reject(error); } } /** * Get QOS getCurrentAppBandwidthByMAC. Only works on R7000 * @returns {Promise.<{ currentDeviceUpBandwidth, currentDeviceDownBandwidth }>} */ async getCurrentBandwidthByMAC(mac) { try { const message = soap.getCurrentBandwidthByMAC(this.sessionId, mac); const result = await this._queueMessage(soap.action.getCurrentBandwidthByMAC, message); const currentDeviceUpBandwidth = regexCurrentDeviceUpBandwidth.exec(result.body)[1]; const currentDeviceDownBandwidth = regexCurrentDeviceDownBandwidth.exec(result.body)[1]; // response: // <m:GetCurrentBandwidthByMACResponse xmlns:m="urn:NETGEAR-ROUTER:service:AdvancedQoS:1"> // <NewCurrentDeviceUpBandwidth>0</NewCurrentDeviceUpBandwidth> // <NewCurrentDeviceDownBandwidth>0</NewCurrentDeviceDownBandwidth> // </m:GetCurrentBandwidthByMACResponse> return Promise.resolve({ currentDeviceUpBandwidth, currentDeviceDownBandwidth }); } catch (error) { return Promise.reject(error); } } /** * Enable or Disable QoS * @param {boolean} enable - true to enable, false to disable. * @returns {Promise<finished>} */ async setQoSEnableStatus(enable) { try { await this._configurationStarted(); const message = soap.setQoSEnableStatus(this.sessionId, enable); await this._queueMessage(soap.action.setQoSEnableStatus, message); await this._configurationFinished() .catch(() => { // console.log(`finished with warning`); }); return Promise.resolve(true); } catch (error) { return Promise.reject(error); } } /** * Get Traffic Meter Enable Status (true / false). * @returns {Promise.<trafficMeterEnabled>} */ async getTrafficMeterEnabled() { try { const message = soap.getTrafficMeterEnabled(this.sessionId); const result = await this._queueMessage(soap.action.getTrafficMeterEnabled, message); const trafficMeterEnabled = regexNewTrafficMeterEnable.exec(result.body)[1] === '1'; return Promise.resolve(trafficMeterEnabled); } catch (error) { return Promise.reject(error); } } /** * Get Traffic Meter options * @returns {Promise.<{newControlOption, newNewMonthlyLimit, restartHour, restartMinute, restartDay}>} */ async getTrafficMeterOptions() { try { const message = soap.getTrafficMeterOptions(this.sessionId); const result = await this._queueMessage(soap.action.getTrafficMeterOptions, message); const newControlOption = regexNewControlOption.exec(result.body)[1]; const newNewMonthlyLimit = Number(regexNewMonthlyLimit.exec(result.body)[1].replace(',', '')); const restartHour = Number(regexRestartHour.exec(result.body)[1]); const restartMinute = Number(regexRestartMinute.exec(result.body)[1]); const restartDay = Number(regexRestartDay.exec(result.body)[1]); const trafficMeterOptions = { newControlOption, newNewMonthlyLimit, restartHour, restartMinute, restartDay, }; return Promise.resolve(trafficMeterOptions); } catch (error) { return Promise.reject(error); } } /** * Enable or Disable Traffic Meter statistics * @param {boolean} enable - true to enable, false to disable. * @returns {Promise<finished>} */ async enableTrafficMeter(enabled) { // true or false try { await this._configurationStarted(); const message = soap.enableTrafficMeter(this.sessionId, enabled); await this._queueMessage(soap.action.enableTrafficMeter, message); await this._configurationFinished() .catch(() => { // console.log(`finished with warning.`); }); return Promise.resolve(true); } catch (error) { return Promise.reject(error); } } /** * Get Bandwidt Control options * @returns {Promise.<{newUplinkBandwidth, newDownlinkBandwidth, enabled}>} */ async getBandwidthControlOptions() { try { const message = soap.getBandwidthControlOptions(this.sessionId); const result = await this._queueMessage(soap.action.getBandwidthControlOptions, message); const newUplinkBandwidth = Number(regexNewUplinkBandwidth.exec(result.body)[1].replace(',', '')); const newDownlinkBandwidth = Number(regexNewDownlinkBandwidth.exec(result.body)[1].replace(',', '')); const enabled = Number(regexNewSettingMethod.exec(result.body)[1]); const bandwidthControlOptions = { newUplinkBandwidth, newDownlinkBandwidth, enabled, }; return Promise.resolve(bandwidthControlOptions); } catch (error) { return Promise.reject(error); } } /** * sets Qos bandwidth options * @param {number} newUplinkBandwidth - maximum uplink bandwidth (Mb/s). * @param {number} newDownlinkBandwidth - maximum downlink bandwidth (Mb/s). * @returns {Promise<finished>} */ async setBandwidthControlOptions(newUplinkBandwidth, newDownlinkBandwidth) { try { await this._configurationStarted(); const message = soap.setBandwidthControlOptions(this.sessionId, newUplinkBandwidth, newDownlinkBandwidth); await this._queueMessage(soap.action.setBandwidthControlOptions, message); await this._configurationFinished() .catch(() => { // console.log(`finished with warning`); }); return Promise.resolve(true); } catch (error) { return Promise.reject(error); } } /** * Get BlockDeviceEnabled status (= device access control) * @returns {Promise<blockDeviceEnabled>} */ async getBlockDeviceEnableStatus() { try { const message = soap.getBlockDeviceEnableStatus(this.sessionId); const result = await this._queueMessage(soap.action.getBlockDeviceEnableStatus, message); const blockDeviceEnabled = regexNewBlockDeviceEnable.exec(result.body)[1] === '1'; return Promise.resolve(blockDeviceEnabled); } catch (error) { return Promise.reject(error); } } /** * Enable or Disable BlockDevice (= device access control) * @param {boolean} enable - true to enable, false to disable. * @returns {Promise<finished>} */ async setBlockDeviceEnable(enable) { try { await this._configurationStarted(); const message = soap.setBlockDeviceEnable(this.sessionId, enable); await this._queueMessage(soap.action.setBlockDeviceEnable, message); await this._configurationFinished() .catch(() => { // console.log(`finished with warning`); }); return Promise.resolve(true); } catch (error) { return Promise.reject(error); } } async enableBlockDeviceForAll() { // deprecated? see setBlockDeviceEnable // enables Block Device (= access control). Rejects if error occurred. // console.log('enableBlockDeviceForAll started'); try { await this._configurationStarted(); const message = soap.enableBlockDeviceForAll(this.sessionId); await this._queueMessage(soap.action.enableBlockDeviceForAll, message); await this._configurationFinished() .catch(() => { // console.log(`finished with warning`); }); return Promise.resolve(true); } catch (error) { return Promise.reject(error); } } /** * Enable or Disable BlockDevice (= device access control) * @param {string} MAC - MAC address of the device to block or allow. * @param {string} AllowOrBlock - either 'Allow' or 'Block'. * @returns {Promise<MAC>} */ async setBlockDevice(MAC, AllowOrBlock) { try { await this._configurationStarted(); const message = soap.setBlockDevice(this.sessionId, MAC, AllowOrBlock); await this._queueMessage(soap.action.setBlockDevice, message); // response code 1 = unknown MAC?, 2= device not connected? await this._configurationFinished() .catch(() => { // console.log(`Block/Allow finished with warning for ${MAC}`); }); return Promise.resolve(MAC); } catch (error) { return Promise.reject(error); } } /** * Get 2.4GHz-1 guest Wifi status * @returns {Promise<enabled>} */ async getGuestWifiEnabled() { try { const message = soap.getGuestAccessEnabled(this.sessionId); const result = await this._queueMessage(soap.action.getGuestAccessEnabled, message); return Promise.resolve(result.body.includes('<NewGuestAccessEnabled>1</NewGuestAccessEnabled>')); } catch (error) { return Promise.reject(error); } } /** * Enable or Disable 2.4GHz-1 guest Wifi * @param {boolean} enable - true to enable, false to disable. * @returns {Promise<wifiSetMethod>} */ async setGuestWifi(enable) { // true or false this.guestWifiMethod.set24_1 = 1; const method = await this._setGuestAccessEnabled(enable) .catch(async () => { // console.log('trying method 2'); this.guestWifiMethod.set24_1 = 2; return this._setGuestAccessEnabled2(enable) .catch((err) => Promise.reject(err)); }); return Promise.resolve(method); } /** * Get 5GHz-1 guest Wifi status * @returns {Promise<enabled>} */ async get5GGuestWifiEnabled() { // console.log('Get 5GHz-1 Guest wifi enabled status'); try { // try method R8000 this.guestWifiMethod.get50_1 = 2; const message = soap.get5G1GuestAccessEnabled(this.sessionId); const result = await this._queueMessage(soap.action.get5G1GuestAccessEnabled2, message) .catch(() => { // console.log('trying alternative method'); // try method R7800 this.guestWifiMethod.get50_1 = 1; return Promise.resolve(this._queueMessage(soap.action.get5G1GuestAccessEnabled, message)); }); return Promise.resolve(result.body.includes('<NewGuestAccessEnabled>1</NewGuestAccessEnabled>')); } catch (error) { return Promise.reject(error); } } /** * Enable or Disable 5GHz-1 guest Wifi * @param {boolean} enable - true to enable, false to disable. * @returns {Promise<wifiSetMethod>} */ async set5GGuestWifi(enable) { // true or false this.guestWifiMethod.set50_1 = 2; const method = await this._set5G1GuestAccessEnabled2(enable) .catch(async () => { // console.log('trying method 2'); this.guestWifiMethod.set50_1 = 1; return this._set5G1GuestAccessEnabled(enable) .catch((err) => Promise.reject(err)); }); return Promise.resolve(method); } /** * Get 5GHz-2 guest Wifi status * @returns {Promise<enabled>} */ async get5GGuestWifi2Enabled() { // console.log('Get 5GHz-1 Guest wifi enabled status'); try { // try method R8000 const message = soap.get5GGuestAccessEnabled2(this.sessionId); const result = await this._queueMessage(soap.action.get5GGuestAccessEnabled2, message); return Promise.resolve(result.body.includes('<NewGuestAccessEnabled>1</NewGuestAccessEnabled>')); } catch (error) { return Promise.reject(error); } } /** * Enable or Disable 5GHz-2 guest Wifi * @param {boolean} enable - true to enable, false to disable. * @returns {Promise<wifiSetMethod>} */ async set5GGuestWifi2(enable) { // true or false const method = await this._set5GGuestAccessEnabled2(enable); return Promise.resolve(method); } /** * Get available Wifi channels * @param {string} [band = '2.4G'] - '2.4G', '5G' or '5G1' * @returns {Promise.<channels[]>} */ async getWifiChannels(band) { // console.log('Get available wifi channels'); try { const message = soap.getAvailableChannel(this.sessionId, band || '2.4G'); const result = await this._queueMessage(soap.action.getAvailableChannel, message); const availableChannels = regexNewAvailableChannel.exec(result.body)[1]; const availableChannelsArray = availableChannels.split(','); return Promise.all(availableChannelsArray); } catch (error) { return Promise.reject(error); } } /** * Set the wifi channel * @param {string} [channel = 'Auto'] - e.g. '6' * @param {string} [band = '2.4G'] - '2.4G', '5G' or '5G1' * @returns {Promise<finished>} */ async setWifiChannel(channel, band) { // console.log('setting wifi channel'); try { const chnl = channel || 'Auto'; const availableChannels = await this.getWifiChannels(band); if (!availableChannels.includes(chnl)) throw Error('Channel is not supported on this band'); await this._configurationStarted(); if (band === '5G') { const message = soap.set5GChannel(this.sessionId, chnl); await this._queueMessage(soap.action.set5GChannel, message); } else if (band === '5G1') { const message = soap.set5G1Channel(this.sessionId, chnl); await this._queueMessage(soap.action.set5G1Channel, message); } else { const message = soap.setChannel(this.sessionId, chnl); await this._queueMessage(soap.action.setChannel, message); } await this._configurationFinished() .catch(() => { // console.log(`setGuestAccessEnabled finished with warning.`); }); return Promise.resolve(true); } catch (error) { return Promise.reject(error); } } /** * Get 2.4G Wifi channel info * @returns {Promise.<channel>} */ async getChannelInfo() { // console.log('Get available 2.4G channel'); try { const message = soap.getChannelInfo(this.sessionId); const result = await this._queueMessage(soap.action.getChannelInfo, message); const channel = regexNewChannel.exec(result.body)[1]; return Promise.resolve(channel); } catch (error) { return Promise.reject(error); } } /** * Get 5G-1 Wifi channel info * @returns {Promise.<channel>} */ async get5GChannelInfo() { // console.log('Get available 5G channel'); try { const message = soap.get5GChannelInfo(this.sessionId); const result = await this._queueMessage(soap.action.get5GChannelInfo, message); const channel5G = regexNew5GChannel.exec(result.body)[1]; return Promise.resolve(channel5G); } catch (error) { return Promise.reject(error); } } /** * Get 5G-2 Wifi channel info * @returns {Promise.<channel>} */ async get5G1ChannelInfo() { // console.log('Get available 5G-2 channel'); try { const message = soap.get5G1ChannelInfo(this.sessionId); const result = await this._queueMessage(soap.action.get5G1ChannelInfo, message); const channel5G1 = regexNew5G1Channel.exec(result.body)[1]; return Promise.resolve(channel5G1); } catch (error) { return Promise.reject(error); } } /** * Get smartConnect Enable Status (true / false). * @returns {Promise.<smartConnectEnabled>} */ async getSmartConnectEnabled() { try { await this._configurationStarted(); const message = soap.getSmartConnectEnabled(this.sessionId); const result = await this._queueMessage(soap.action.getSmartConnectEnabled, message); await this._configurationFinished() .catch(() => { // console.log(`finished with warning`); }); const smartConnectEnabled = regexNewSmartConnectEnable.exec(result.body)[1] === '1'; return Promise.resolve(smartConnectEnabled); } catch (error) { return