netgear
Version:
Node module to interact with Netgear routers via SOAP
1,369 lines (1,291 loc) • 86.4 kB
JavaScript
/* 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(/&/gi, '&')
.replace(/</gi, '<')
.replace(/>/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