iobroker.fb-checkpresence
Version:
The adapter checks the presence of family members over the fritzbox. You must fill in the name of the family member and the mac-address of the used device. The comment is optional and you can enable or disable the family member. The datapoint based on the
1,186 lines (1,154 loc) • 48.1 kB
JavaScript
;
const xml2jsP = require('xml2js');
const https = require('https');
const axios = require('axios');
const crypto = require('crypto');
const qr = require('qr-image');
class exceptionFb extends Error {
constructor(name, message) {
super(message);
this.name = name;
this.toString = function () {
return `${this.name}: ${this.message}`;
};
}
}
class exceptionSoapCommand extends Error {
constructor(name, message) {
super(message);
this.name = name;
this.toString = function () {
return `${this.name}: ${this.message}`;
};
}
}
class exceptionFbSendWithRetry extends Error {
constructor(message) {
super(message);
this.name = 'exceptionFbSendWithRetry';
this.toString = function () {
return `${this.name}: ${this.message}`;
};
}
}
/**
* A class to use the fritzbox api
*
*/
class Fb {
/**
* @param {{ host: any; uid: any; pwd: any; }} deviceInfo - configuration items
* @param {any} ssl - configuration item
* @param {any} adapter - adapter object
*/
constructor(deviceInfo, ssl, adapter) {
this._urn = 'urn:dslforum-org:service:';
this._MAX_RETRY = 3;
this.adapter = adapter;
this.modelName = null;
this.version = null;
this._sslPort = null;
this.host = deviceInfo.host;
this.port = 49000;
this.timeout = 6000;
this._auth = {
uid: deviceInfo.uid,
pwd: deviceInfo.pwd,
sn: null,
auth: null,
realm: 'F!Box SOAP-Auth',
chCount: 0,
};
this.agent = null; //new https.Agent({ defaultPort: 43213, keepAlive: true, rejectUnauthorized: false });
this.controller = null;
this.source = null;
try {
this.controller = new AbortController();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
this.controller = null;
this.source = axios.CancelToken.source();
}
this.ssl = ssl;
this.serviceUrl = `http://${this.host}:${this.port}/tr64desc.xml`;
this.services = null;
this.currentRetry = 0;
this.deviceList = null;
this.meshList = null;
this.accessRights = { C: 'none', P: 'none', D: 'none', N: 'none', H: 'none', A: 'none', '-': 'readwrite' };
this.accessType = null; //DSL, Ethernet, X_AVM-DE_Fiber, X_AVMDE_UMTS, X_AVM-DE_Cable, X_AVM-DE_LTE
this.ssid = null;
this.beaconType = null;
this.connection = null; //1.WANPPPConnection.1,
//http://fritz.box:49000/tr64desc.xml
this.suportedServices = [];
this.GETPATH = false;
this.GETMESHPATH = false;
this.GETBYMAC = false;
this.GETBYIP = false;
this.GETPORT = false;
this.GETEXTIP = false;
this.GETEXTIPBYIP = false;
this.GETACCESSTYPE = false;
this.SETENABLE = false;
this.WLAN3INFO = false;
this.WLAN3GETSECKEY = false;
this.WLAN4INFO = false;
this.WLAN4GETSECKEY = false;
this.DEVINFO = false;
this.GETWANACCESSBYIP = false;
this.DISALLOWWANACCESSBYIP = false;
this.REBOOT = false;
this.RECONNECT = false;
this.USERRIGHTS = false;
this.CONNECTION = false;
}
/**
* init function for Fb class
*
* @param deviceInfo - configuration parameters for fritzbox authentification
* @param ssl - ssl configuration parameter
* @param adapter - adapter object
*/
static init(deviceInfo, ssl, adapter) {
return (async function () {
try {
const x = new Fb(deviceInfo, ssl, adapter);
await x._getSSLPort();
if (x._sslPort && x.ssl) {
x.agent = new https.Agent({
rejectUnauthorized: false,
});
}
await x._getAccessRights();
const res = await x._getServices();
if (res === true) {
await x._checkServices();
}
if (x.GETACCESSTYPE && x.GETACCESSTYPE === true) {
await x._getWANAccessType();
}
if (x.CONNECTION && x.CONNECTION === true) {
await x._getDefaultConnection();
}
return x;
} catch (error) {
throw new exceptionSoapCommand('Fb init', error);
}
})();
}
/**
* fritzbox action GetDefaultConnectionService
*/
async _getDefaultConnection() {
try {
const soapResult = await this.soapAction(
'/upnp/control/layer3forwarding',
'urn:dslforum-org:service:' + 'Layer3Forwarding:1',
'GetDefaultConnectionService',
null,
);
if (soapResult) {
this.connection = soapResult['NewDefaultConnectionService'];
return;
}
throw new exceptionSoapCommand(
'getDefaultConnection',
`Cannot get default connection from Fritzbox! ${JSON.stringify(soapResult)}`,
);
} catch (error) {
throw new exceptionFb('getDefaultConnection', error);
}
}
/**
* fritzbox action X_AVM-DE_GetCurrentUser to get access rights
*/
async _getAccessRights() {
try {
const soapResult = await this.soapAction(
'/upnp/control/lanconfigsecurity',
'urn:dslforum-org:service:' + 'LANConfigSecurity:1',
'X_AVM-DE_GetCurrentUser',
null,
);
if (soapResult) {
const result = await xml2jsP.parseStringPromise(soapResult['NewX_AVM-DE_CurrentUserRights'], {
explicitArray: false,
});
if (result && result.rights && result.rights.path) {
for (let i = 0; i < result.rights.path.length; i++) {
switch (result.rights.path[i]) {
case 'BoxAdmin':
this.accessRights.C = result.rights.access[i];
break;
case 'Phone':
this.accessRights.P = result.rights.access[i];
break;
case 'Dial':
this.accessRights.D = result.rights.access[i];
break;
case 'NAS':
this.accessRights.N = result.rights.access[i];
break;
case 'HomeAuto':
this.accessRights.H = result.rights.access[i];
break;
case 'App':
this.accessRights.A = result.rights.access[i];
break;
default:
this.accessRights.none = '-';
}
}
return '';
}
throw new Error('Cannot parse soap result! ');
} else {
throw new exceptionSoapCommand('getAccessRights', 'Cannot get access rights from Fritzbox!');
}
} catch (error) {
if (error.message.includes('authentification failure')) {
throw new exceptionSoapCommand('_getAccessRights', 'please check the user or password!'); //this.adapter.log.warn('please check the user or password!');
} else {
throw new exceptionSoapCommand('_getAccessRights', error);
}
}
}
/**
* fritzbox action to get supported services
*/
async _getServices() {
try {
const para =
this.controller === null
? {
url: this.serviceUrl,
method: 'get',
timeout: this.timeout,
responseType: 'json',
responseEncoding: 'utf8',
cancelToken: this.source.token,
}
: {
url: this.serviceUrl,
method: 'get',
timeout: this.timeout,
responseType: 'json',
responseEncoding: 'utf8',
signal: this.controller.signal,
};
const response = await axios(para);
if (response && response.status == 200) {
this.services = await xml2jsP.parseStringPromise(response.data, { explicitArray: false });
this.modelName = this.services.root.device.modelName;
if (this.services.root.systemVersion != null) {
this.version = this.services.root.systemVersion.Display;
} else {
this.version = 'not defined';
}
return true;
}
throw Error(`Can not read services! Status: ${JSON.stringify(response)}`);
} catch (error) {
this.services = null;
throw new exceptionFb('getServices', error);
}
}
/**
* fritzbox action to get services
*
* @param device - device
* @param serviceId - service id
*/
_getService(device, serviceId) {
const device1 = device;
const dlength = device1.length;
if (device1 && device1.length == undefined) {
const length = device1['serviceList']['service'].length;
for (let s = 0; s < length; s++) {
const service = device1['serviceList']['service'][s];
if (service.serviceId.includes(serviceId)) {
return service;
}
}
if (device1.deviceList && device1.deviceList.device) {
return this._getService(device1.deviceList.device, serviceId);
}
} else {
for (let d = 0; d < dlength; d++) {
const length = device1[d]['serviceList']['service'].length;
const dev = device1[d]['serviceList']['service'];
for (let s = 0; s < length; s++) {
const service = dev[s];
if (service.serviceId.includes(serviceId)) {
return service;
}
}
if (device1[d].deviceList && device1[d].deviceList.device) {
return this._getService(device1[d]['deviceList']['device'], serviceId);
}
}
}
return null;
}
/**
* check service if it is supported by fritzbox
*/
async _checkServices() {
try {
this.GETPATH = await this._chkService('X_AVM-DE_GetHostListPath', 'Hosts1', 'X_AVM-DE_GetHostListPath', [
'C',
'A',
'P',
]);
this.GETMESHPATH = await this._chkService(
'X_AVM-DE_GetMeshListPath',
'Hosts1',
'X_AVM-DE_GetMeshListPath',
['C', 'A', 'P'],
);
this.GETBYMAC = await this._chkService('GetSpecificHostEntry', 'Hosts1', 'GetSpecificHostEntry', ['-']);
this.GETBYIP = await this._chkService(
'X_AVM-DE_GetSpecificHostEntryByIP',
'Hosts1',
'X_AVM-DE_GetSpecificHostEntryByIP',
['C', 'A', 'P'],
);
this.GETPORT = await this._chkService('GetSecurityPort', 'DeviceInfo1', 'GetSecurityPort', ['-']);
this.GETACCESSTYPE = await this._chkService(
'GetCommonLinkProperties',
'WANCommonInterfaceConfig1',
'GetCommonLinkProperties',
['C'],
);
this.GETEXTIP = await this._chkService('GetInfo', 'WANPPPConnection1', 'GetInfo', ['C']);
this.GETEXTIPBYIP = await this._chkService('GetInfo', 'WANIPConnection1', 'GetInfo', ['C']);
this.SETENABLE = await this._chkService('SetEnable', 'WLANConfiguration3', 'SetEnable', ['A']);
this.WLAN3INFO = await this._chkService('GetInfo', 'WLANConfiguration3', 'WLANConfiguration3-GetInfo', [
'C',
'A',
'P',
]);
this.WLAN3GETSECKEY = await this._chkService(
'GetSecurityKeys',
'WLANConfiguration3',
'WLANConfiguration3-GetSecurityKeys',
['A'],
);
this.WLAN4INFO = await this._chkService('GetInfo', 'WLANConfiguration4', 'WLANConfiguration4-GetInfo', [
'C',
'A',
'P',
]);
this.WLAN4GETSECKEY = await this._chkService(
'GetSecurityKeys',
'WLANConfiguration4',
'WLANConfiguration4-GetSecurityKeys',
['A'],
);
this.DEVINFO = await this._chkService('GetInfo', 'DeviceInfo1', 'DeviceInfo1-GetInfo', ['C']);
this.DISALLOWWANACCESSBYIP = await this._chkService(
'DisallowWANAccessByIP',
'X_AVM-DE_HostFilter',
'DisallowWANAccessByIP',
['C', 'A'],
);
this.GETWANACCESSBYIP = await this._chkService(
'GetWANAccessByIP',
'X_AVM-DE_HostFilter',
'GetWANAccessByIP',
['C', 'A'],
);
this.REBOOT = await this._chkService('Reboot', 'DeviceConfig1', 'Reboot', ['C']);
this.RECONNECT = await this._chkService('ForceTermination', 'WANPPPConnection1', 'ForceTermination', ['C']);
this.USERRIGHTS = await this._chkService(
'X_AVM-DE_GetCurrentUser',
'LANConfigSecurity1',
'X_AVM-DE_GetCurrentUser',
['C', 'A', 'P', 'N', 'H'],
);
this.CONNECTION = await this._chkService(
'GetDefaultConnectionService',
'Layer3Forwarding1',
'GetDefaultConnectionService',
['C'],
);
} catch (error) {
throw new exceptionFb('checkServices', error);
}
}
/**
*
* @param action - action name
* @param serviceId - service id
* @param dp - data point
* @param neededAccessRights - needed access rights for correct function
*/
async _chkService(action, serviceId, dp, neededAccessRights) {
try {
if (this.services === null) {
throw Error('can not get services!');
}
const service = this._getService(this.services['root']['device'], serviceId);
if (service) {
const nurl = `http://${this.host}:${this.port}${service.SCPDURL}`;
const para =
this.controller === null
? {
url: nurl,
method: 'get',
timeout: this.timeout,
responseType: 'json',
responseEncoding: 'utf8',
cancelToken: this.source.token,
}
: {
url: nurl,
method: 'get',
timeout: this.timeout,
responseType: 'json',
responseEncoding: 'utf8',
signal: this.controller.signal,
};
const response = await axios(para);
if (response && response.status == 200) {
const found = JSON.stringify(response.data).search(action);
if (found == -1) {
this.suportedServices.push({ id: dp, name: `${serviceId}-${action}`, enabled: false });
return false;
}
let supported = false;
for (let i = 0; i < neededAccessRights.length; i++) {
const right = this.accessRights[neededAccessRights[i]];
if (right != 'none') {
supported = true;
break;
}
}
this.suportedServices.push({ id: dp, name: `${serviceId}-${action}`, enabled: supported });
return supported;
}
this.adapter.log.info(
`service ${serviceId}-${action} is not supported! Can not find action! Feature is deactivated! `,
);
return false;
}
this.adapter.log.info(
`service ${serviceId}-${action} is not supported! Can not find service file! Feature is deactivated`,
);
return false;
} catch (error) {
throw new exceptionFb('checkService', `${error.name} ${error.message}`);
}
}
/**
* read ssl port from fritzbox
*/
async _getSSLPort() {
try {
const soapResult = await this.soapAction(
'/upnp/control/deviceinfo',
'urn:dslforum-org:service:' + 'DeviceInfo:1',
'GetSecurityPort',
null,
);
if (soapResult) {
const sslPort = parseInt(soapResult['NewSecurityPort']);
if (typeof sslPort === 'number' && isFinite(sslPort)) {
this._sslPort = sslPort;
return sslPort;
}
throw new Error(`Cannot get ssl port! Wrong type. ${JSON.stringify(soapResult)}`);
} else {
throw new exceptionSoapCommand(
'GetSecurityPort',
`Cannot get ssl port from Fritzbox! ${JSON.stringify(soapResult)}`,
);
}
} catch (error) {
this._sslPort = null;
if (error.message.includes('timeout')) {
this.adapter.log.warn(
'The user could have been blocked from the fritzbox due to authentification problems! Please stop the adapter and reboot the fritzbox. Then start and try again.',
);
throw new exceptionFb('getSSLPort', error);
} else {
throw new exceptionFb('getSSLPort', error);
}
}
}
/**
*
* @param url - soap url
* @param timeout - timeout
* @param start - retry counter
* @param message - message
*/
async _sendWithRetry(url, timeout, start, message) {
this.currentRetry = start;
try {
const para =
this.controller === null
? {
url: url,
method: 'get',
timeout: this.timeout,
responseType: 'text',
responseEncoding: 'utf8',
httpsAgent: this.agent,
cancelToken: this.source.token,
}
: {
url: url,
method: 'get',
timeout: this.timeout,
responseType: 'text',
responseEncoding: 'utf8',
httpsAgent: this.agent,
signal: this.controller.signal,
};
const response = await axios(para);
return response;
} catch (error) {
if (error.message && error.message == 'Request canceled!') {
return false;
}
if (this.currentRetry < this._MAX_RETRY) {
this.currentRetry++;
return await this._sendWithRetry(url, timeout, this.currentRetry, message);
}
this.currentRetry = 0;
throw new exceptionFbSendWithRetry(error);
}
}
/**
* read fritzbox device list with action X_AVM-DE_GetHostListPath
*/
async getDeviceList() {
try {
const soapResult = await this.soapAction(
'/upnp/control/hosts',
'urn:dslforum-org:service:' + 'Hosts:1',
'X_AVM-DE_GetHostListPath',
null,
);
if (soapResult) {
let url = null;
if (this._sslPort && this.ssl) {
url = `https://${this.host}:${this._sslPort}${soapResult['NewX_AVM-DE_HostListPath']}`;
} else {
url = `http://${this.host}:${this.port}${soapResult['NewX_AVM-DE_HostListPath']}`;
}
const para =
this.controller === null
? {
url: url,
method: 'get',
timeout: this.timeout + 4000,
responseType: 'text',
responseEncoding: 'utf8',
httpsAgent: this.agent,
cancelToken: this.source.token,
}
: {
url: url,
method: 'get',
timeout: this.timeout + 4000,
responseType: 'text',
responseEncoding: 'utf8',
httpsAgent: this.agent,
signal: this.controller.signal,
};
const response = await axios(para);
if (response && response.status == 200 && response.data) {
let deviceList = await xml2jsP.parseStringPromise(response.data, { explicitArray: false });
//this.adapter.log.warn('devicelist: ' + JSON.stringify(deviceList));
if (deviceList) {
this.deviceList = deviceList['List']['Item'];
deviceList = null;
return true;
}
throw new Error(`Cannot parse response.data ${JSON.stringify(response.data)}`);
} else {
if (response === false) {
return false;
}
throw new exceptionFb('exceptionHostlist', 'Cannot get hostlist');
}
} else {
throw new exceptionSoapCommand('X_AVM-DE_GetHostListPath', 'Cannot get hostlist path from Fritzbox!');
}
} catch (error) {
if (error.name == 'CanceledError') {
throw new exceptionSoapCommand('getDeviceList', error.message);
} else {
throw new exceptionSoapCommand('getDeviceList', `${error.name} ${error.message}`);
}
}
}
/**
* read fritzbox mesh list with action X_AVM-DE_GetMeshListPath
*/
async getMeshList() {
try {
const soapResult = await this.soapAction(
'/upnp/control/hosts',
'urn:dslforum-org:service:' + 'Hosts:1',
'X_AVM-DE_GetMeshListPath',
null,
);
if (soapResult) {
let url = null;
if (this._sslPort && this.ssl) {
url = `https://${this.host}:${this._sslPort}${soapResult['NewX_AVM-DE_MeshListPath']}`;
} else {
url = `http://${this.host}:${this.port}${soapResult['NewX_AVM-DE_MeshListPath']}`;
}
const para =
this.controller === null
? {
url: url,
method: 'get',
timeout: this.timeout + 8000,
responseType: 'json',
responseEncoding: 'utf8',
httpsAgent: this.agent,
cancelToken: this.source.token,
}
: {
url: url,
method: 'get',
timeout: this.timeout + 8000,
responseType: 'json',
responseEncoding: 'utf8',
httpsAgent: this.agent,
signal: this.controller.signal,
};
const response = await axios(para);
if (response != null && response.status == 200 && response.data) {
this.meshList = response.data['nodes'];
return true;
}
if (response === false) {
return false;
}
throw new Error('Cannot get mesh list');
} else {
throw new exceptionSoapCommand('X_AVM-DE_GetMeshListPath', 'Cannot get mesh list path');
}
} catch (error) {
if (error.name == 'CanceledError') {
throw new exceptionSoapCommand('getMeshList', error.message);
} else {
throw new exceptionSoapCommand('getMeshList', `${error.name} ${error.message}`);
}
}
}
/**
* read fritzbox wan access type with action GetCommonLinkProperties
*/
async _getWANAccessType() {
try {
const soapResult = await this.soapAction(
'/upnp/control/wancommonifconfig1',
'urn:dslforum-org:service:' + 'WANCommonInterfaceConfig:1',
'GetCommonLinkProperties',
null,
);
if (soapResult) {
this.accessType = soapResult['NewWANAccessType'];
} else {
throw new exceptionSoapCommand('GetCommonLinkProperties', 'Cannot get wan access type from Fritzbox!');
}
} catch (error) {
throw new exceptionFb('getWANAccessType', `${error.name} ${error.message}`);
}
}
//Get external IP address
/**
* read external ip from fritzbox
*/
async getExtIp() {
try {
let soapResult = null;
if (this.GETEXTIP && this.GETEXTIP == true) {
if (this.connection == '1.WANPPPConnection.1') {
soapResult = await this.soapAction(
'/upnp/control/wanpppconn1',
'urn:dslforum-org:service:' + 'WANPPPConnection:1',
'GetInfo',
null,
);
} else {
soapResult = await this.soapAction(
'/upnp/control/wanipconnection1',
'urn:dslforum-org:service:' + 'WANIPConnection:1',
'GetInfo',
null,
);
}
if (soapResult) {
return soapResult['NewExternalIPAddress'];
}
const mesg = soapResult == null ? '' : soapResult;
throw new exceptionSoapCommand(
'GetInfo WANConnection',
`Cannot get external ip address ${JSON.stringify(mesg)}`,
);
}
} catch (error) {
this.adapter.log.warn(`getExtIp: ${error.name} ${error.message}`);
return '';
}
}
//Get external IPv6 address
/**
* read external ipv6 from fritzbox
*/
async getExtIpv6() {
try {
let soapResult = null;
if (this.GETEXTIP && this.GETEXTIP == true) {
if (this.connection == '1.WANPPPConnection.1') {
soapResult = await this.soapAction(
'/igdupnp/control/WANIPConn1',
'urn:schemas-upnp-org:service:' + 'WANIPConnection:1',
'X_AVM_DE_GetExternalIPv6Address',
null,
);
} else {
soapResult = await this.soapAction(
'/igdupnp/control/WANIPConn1',
'urn:schemas-upnp-org:service:' + 'WANIPConnection:1',
'X_AVM_DE_GetExternalIPv6Address',
null,
);
}
if (soapResult) {
return soapResult['NewExternalIPv6Address'];
}
const mesg = soapResult == null ? '' : soapResult;
throw new exceptionSoapCommand(
'X_AVM_DE_GetExternalIPv6Address',
`Cannot get external ipv6 address ${JSON.stringify(mesg)}`,
);
}
} catch (error) {
this.adapter.log.warn(`getExtIpv6: ${error.name} ${error.message}`);
return '';
}
}
//Get external IPv6 address
/**
* read external ipv6 from fritzbox
*/
async getExtIpv6Prefix() {
try {
let soapResult = null;
if (this.GETEXTIP && this.GETEXTIP == true) {
if (this.connection == '1.WANPPPConnection.1') {
soapResult = await this.soapAction(
'/igdupnp/control/WANIPConn1',
'urn:schemas-upnp-org:service:' + 'WANIPConnection:1',
'X_AVM_DE_GetIPv6Prefix',
null,
);
} else {
soapResult = await this.soapAction(
'/igdupnp/control/WANIPConn1',
'urn:schemas-upnp-org:service:' + 'WANIPConnection:1',
'X_AVM_DE_GetIPv6Prefix',
null,
);
}
if (soapResult) {
return soapResult['NewIPv6Prefix'];
}
const mesg = soapResult == null ? '' : soapResult;
throw new exceptionSoapCommand(
'X_AVM_DE_GetIPv6Prefix',
`Cannot get external ipv6 prefix ${JSON.stringify(mesg)}`,
);
}
} catch (error) {
this.adapter.log.warn(`getExtIpv6Prefix: ${error.name} ${error.message}`);
return '';
}
}
//Get status of guest wlan
/**
* read guest wlan info
*/
async getGuestWlan() {
try {
let serviceType = '';
let controlURL = '';
if (serviceType == '' && this.WLAN4INFO && this.WLAN4INFO == true) {
serviceType = 'urn:dslforum-org:service:WLANConfiguration:4';
controlURL = '/upnp/control/wlanconfig4';
}
if (serviceType == '' && this.WLAN3INFO && this.WLAN3INFO == true) {
serviceType = 'urn:dslforum-org:service:WLANConfiguration:3';
controlURL = '/upnp/control/wlanconfig3';
}
const soapResult = await this.soapAction(controlURL, serviceType, 'GetInfo', null);
if (soapResult) {
const wlanStatus = soapResult['NewEnable'] == 1 ? true : false;
this.ssid = soapResult['NewSSID'];
this.beaconType = soapResult['NewBeaconType'];
return wlanStatus;
}
throw new exceptionSoapCommand(
'GetInfo WLANConfiguration',
`Cannot get status of guest wlan ${JSON.stringify(soapResult)}`,
);
} catch (error) {
//throw new exceptionFb('getGuestWlan', error.name + ' ' + error.message);
this.adapter.log.warn(`getGuestWlan ${error.name} ${error.message}`);
return;
}
}
//Get status of guest wlan
/**
* convert guest wlan info into qr-code
*/
async getGuestQR() {
try {
let serviceType = '';
let controlURL = '';
if (serviceType == '' && this.WLAN4GETSECKEY === true) {
serviceType = 'urn:dslforum-org:service:WLANConfiguration:4';
controlURL = '/upnp/control/wlanconfig4';
}
if (serviceType == '' && this.WLAN3GETSECKEY === true) {
serviceType = 'urn:dslforum-org:service:WLANConfiguration:3';
controlURL = '/upnp/control/wlanconfig3';
}
const soapResult = await this.soapAction(controlURL, serviceType, 'GetSecurityKeys', null);
if (soapResult) {
let password = soapResult['NewKeyPassphrase'];
password = password.replace(/[\\";:,]/g, '\\$&');
const SSID = this.ssid.replace(/[\\";:,]/g, '\\$&');
let WPA = null; //this.beaconType == '11i' ? 'WPA2' : this.beaconType;
switch (this.beaconType) {
case 'None':
WPA = 'nopass';
break;
case 'Basic':
WPA = 'WEP';
break;
case 'WPA':
WPA = 'WPA';
break;
case '11i':
WPA = 'WPA';
break;
case 'WPAand11i':
WPA = 'WPA';
break;
case 'WPA3':
WPA = 'WPA';
break;
case '11iandWPA3':
WPA = 'WPA';
break;
case 'OWE':
WPA = 'WPA';
break;
case 'OWETrans':
WPA = 'WPA';
break;
default:
WPA = 'nopass';
break;
}
const qrcode = `WIFI:T:${WPA};P:${password};S:${SSID};;`;
let svg = qr.imageSync(qrcode, { type: 'svg' });
svg = svg.replace('<path ', `<path fill="${this.adapter.config.qrcodecolor}" `);
//svg.pipe(require('fs').createWriteStream('/tmp/qr.png', svg));
//this.adapter.writeFileAsync('vis.0','/main/qr.png', svg);
return svg;
}
throw new exceptionSoapCommand(
'getGuestQR',
`Cannot get datamatrix code of guest wlan ${JSON.stringify(soapResult)}`,
);
} catch (error) {
this.adapter.log.warn(`getGuestQR ${error.name} ${error.message}`);
return;
}
}
/**
* read status of wan access for the given device
*
* @param ipaddress - ip address of the given device
*/
async getWanAccess(ipaddress) {
try {
const soapResult = await this.soapAction(
'/upnp/control/x_hostfilter',
'urn:dslforum-org:service:X_AVM-DE_HostFilter:1',
'GetWANAccessByIP',
[[1, 'NewIPv4Address', ipaddress.val]],
);
if (soapResult?.NewDisallow !== undefined) {
return soapResult.NewDisallow;
}
// Exception werfen, falls kein valider SOAP-Resultatwert vorhanden ist
throw new exceptionSoapCommand(
'getWanAccess',
`Cannot get WAN access status: ${JSON.stringify(soapResult)}`,
);
} catch (error) {
this.adapter.log.warn(`getWanAccess: ${error.name} ${error.message}`);
return null; // Kein weiterer Throw; Rückgabe von null bei Fehlern
}
}
/**
* check connection to fritzbox
*
*/
async connectionCheck() {
try {
const soapResult = await this.soapAction(
'/upnp/control/hosts',
'urn:dslforum-org:service:' + 'Hosts:1',
'GetHostNumberOfEntries',
null,
);
return !!soapResult; // Wenn soapResult nicht null oder undefiniert ist, wird true zurückgegeben, ansonsten false.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (error) {
return false;
}
}
// Login
/**
* calculate hash for authentification
*
* @param uid - user id
* @param pwd - password
* @param realm - realm
* @param sn - sn
*/
_calcAuthDigest(uid, pwd, realm, sn) {
const secret = crypto.createHash('md5').update(`${uid}:${realm}:${pwd}`).digest('hex');
return crypto.createHash('md5').update(`${secret}:${sn}`).digest('hex');
}
/**
* exit axios request function
*
*/
exitRequest() {
if (this.controller !== null) {
this.controller.abort();
}
if (this.source !== null) {
this.source.cancel('Operation canceled!');
}
}
// Soap query
/**
* soap action function
*
* @param url - soap url
* @param serviceType - fritzbox api service type
* @param action - fritzbox api action
* @param vars - fritzbox api parameters for action
*/
async soapAction(url, serviceType, action, vars) {
const service = serviceType.replace(this._urn, '');
try {
let head = '';
if (this._auth.uid) {
// Content Level Authentication
if (this._auth.auth) {
head =
`<s:Header>` +
`<h:ClientAuth xmlns:h="http://soap-authentication.org/digest/2001/10/"` +
`s:mustUnderstand="1">` +
`<Nonce>${this._auth.sn}</Nonce>` +
`<Auth>${this._auth.auth}</Auth>` +
`<UserID>${this._auth.uid}</UserID>` +
`<Realm>${this._auth.realm}</Realm>` +
`</h:ClientAuth>` +
`</s:Header>`;
} else {
// First Auth
head =
` <s:Header>` +
`<h:InitChallenge xmlns:h="http://soap-authentication.org/digest/2001/10/"` +
`s:mustUnderstand="1">` +
`<UserID>${this._auth.uid}</UserID>` +
`</h:InitChallenge>` +
`</s:Header>`;
}
}
let body =
`<?xml version="1.0" encoding="utf-8"?>` +
`<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">${
head
}<s:Body>` +
`<u:${action} xmlns:u="${serviceType}">`;
//insert parameters
if (vars != null) {
vars.forEach(function (item) {
body += `<${item[1]}>`;
body += item[2];
body += `</${item[1]}>`;
});
}
body = `${body}</u:${action}>` + `</s:Body>` + `</s:Envelope>`;
let port = 0;
let proto = '';
if (this._sslPort && this._auth.auth && this.ssl) {
port = this._sslPort;
proto = 'https://';
} else {
proto = 'http://';
port = this.port;
}
const uri = `${proto + this.host}:${port}${url}`;
const para =
this.controller === null
? {
method: 'post',
data: body,
timeout: this.timeout,
proxy: false,
responseType: 'text',
httpsAgent: this.agent,
headers: {
SoapAction: `${serviceType}#${action}`,
'Content-Type': 'text/xml;charset=UTF-8',
},
cancelToken: this.source.token,
}
: {
method: 'post',
data: body,
timeout: this.timeout,
proxy: false,
responseType: 'text',
httpsAgent: this.agent,
headers: {
SoapAction: `${serviceType}#${action}`,
'Content-Type': 'text/xml;charset=UTF-8',
},
signal: this.controller.signal,
};
const response = await axios(uri, para);
if (response.status == 200) {
let result = await xml2jsP.parseStringPromise(response.data, { explicitArray: false });
const env = result['s:Envelope'];
result = null;
if (env['s:Header']) {
const header = env['s:Header'];
if (header['h:Challenge']) {
const ch = header['h:Challenge'];
if (this._auth.chCount >= 2) {
this._auth.chCount = 0;
this._auth.auth = null;
//response.data = null;
throw new exceptionSoapCommand('authentification failure', 'Wrong user or password');
} else {
this._auth.sn = ch.Nonce;
this._auth.realm = ch.Realm;
this._auth.auth = this._calcAuthDigest(
this._auth.uid,
this._auth.pwd,
this._auth.realm,
this._auth.sn,
);
this._auth.chCount++;
// Repeat request
//response.data = null;
return await this.soapAction(url, serviceType, action, vars);
//return; //response2;
}
} else if (header['h:NextChallenge']) {
const nx = header['h:NextChallenge'];
//this._auth.auth = nx.Nonce;
this._auth.sn = nx.Nonce;
this._auth.realm = nx.Realm;
this._auth.auth = this._calcAuthDigest(
this._auth.uid,
this._auth.pwd,
this._auth.realm,
this._auth.sn,
);
this._auth.chCount = 0;
}
}
if (env['s:Body']) {
const body = env['s:Body'];
if (body[`u:${action}Response`]) {
//soapResult.data = body['u:' + action + 'Response'];
//response = null;
return body[`u:${action}Response`];
} else if (body['s:Fault']) {
//response.data = null;
throw new exceptionSoapCommand('Fault', body['s:Fault']);
}
}
} else {
//response status different to 200
const mes = JSON.stringify(response.data);
//response.data = null;
throw new exceptionSoapCommand('negative response', mes);
}
} catch (error) {
if (error.name === 'CanceledError') {
throw new exceptionSoapCommand(`${service} ${action}`, error.message);
}
if (error.name === 'AxiosError') {
if (error.response) {
if (error.response.data) {
const errResult = await xml2jsP.parseStringPromise(error.response.data, {
explicitArray: false,
});
if (errResult['s:Envelope']) {
if (errResult['s:Envelope']['s:Body']) {
if (errResult['s:Envelope']['s:Body']['s:Fault']) {
if (errResult['s:Envelope']['s:Body']['s:Fault']['detail']) {
if (errResult['s:Envelope']['s:Body']['s:Fault']['detail']['UPnPError']) {
const errMsg =
errResult['s:Envelope']['s:Body']['s:Fault']['detail']['UPnPError']
.errorDescription;
throw new exceptionSoapCommand(
`${service} ${action}: ${error.response.statusText} -> ${errMsg}`,
error,
);
}
}
}
}
}
throw new exceptionSoapCommand(`${service} ${action}: ${error.response.statusText}`, error);
} else {
throw new exceptionSoapCommand(`${service} ${action}: ${error.response.statusText}`, error);
}
} else {
throw new exceptionSoapCommand(`${service} ${action}`, error);
}
}
if (error.response) {
// The request was made and the server responded with a status code that falls out of the range of 2xx
const errResult = await xml2jsP.parseStringPromise(error.response.data, { explicitArray: false });
const errMsg = errResult['s:Envelope']['s:Body']['s:Fault']['detail']['UPnPError'];
throw new exceptionSoapCommand(`${service} ${action}`, errMsg.errorDescription);
} else if (error.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of http.ClientRequest in node.js
throw new exceptionSoapCommand(`${service} ${action}`, error.message);
} else {
if (error.message && error.message == 'Request canceled!') {
throw new exceptionSoapCommand(`${service} ${action}`, error.message);
}
// Something happened in setting up the request that triggered an Error
throw new exceptionSoapCommand(`${service} ${action}`, `${error.name} ${error.message}`);
}
}
}
}
exports.Fb = Fb;