UNPKG

iobroker.kisshome-research

Version:
1,061 lines (1,060 loc) 61 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.KISSHomeResearchAdapter = void 0; const adapter_core_1 = require("@iobroker/adapter-core"); const axios_1 = __importDefault(require("axios")); const node_path_1 = require("node:path"); const node_crypto_1 = require("node:crypto"); const node_fs_1 = require("node:fs"); const utils_1 = require("./lib/utils"); const recording_1 = require("./lib/recording"); const fritzbox_1 = require("./lib/fritzbox"); const PCAP_HOST = 'kisshome-experiments.if-is.net'; // save files every 60 minutes const SAVE_DATA_EVERY_MS = 3600000; // save files if bigger than 50 Mb const SAVE_DATA_IF_BIGGER = 50 * 1024 * 1024; const SYNC_INTERVAL = 3600000; // 3_600_000; const BACKUP_KEYS = '0_userdata.0.kisshomeResearchKeys'; function size2text(size) { if (size < 1024) { return `${size} B`; } if (size < 1024 * 1024) { return `${Math.round((size * 10) / 1024) / 10} kB`; } return `${Math.round((size * 10) / (1024 * 1024) / 10)} MB`; } class KISSHomeResearchAdapter extends adapter_core_1.Adapter { constructor(options = {}) { super({ ...options, name: 'kisshome-research', useFormatDate: true, }); this.tempDir = ''; this.uniqueMacs = []; this.sid = ''; this.sidCreated = 0; this.context = { terminate: false, controller: null, packets: [], totalBytes: 0, totalPackets: 0, buffer: Buffer.from([]), modifiedMagic: false, libpCapFormat: false, networkType: 1, started: 0, lastSaved: 0, }; this.recordingRunning = false; this.workingDir = ''; this.lastDebug = 0; this.syncRunning = false; this.syncTimer = null; this.publicKey = ''; this.uuid = ''; this.recordingEnabled = false; this.IPs = []; const pack = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, '..', 'package.json'), 'utf8')); this.versionPack = pack.version.replace(/\./g, '-'); this.on('ready', () => this.onReady()); this.on('unload', callback => this.onUnload(callback)); this.on('message', this.onMessage.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); } async onMessage(msg) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q; if (typeof msg === 'object' && msg.message) { switch (msg.command) { case 'getDefaultGateway': if (msg.callback) { if (msg.message.value !== '0.0.0.0') { this.sendTo(msg.from, msg.command, msg.message.value, msg.callback); } else { try { const ip = await (0, utils_1.getDefaultGateway)(); this.sendTo(msg.from, msg.command, ip, msg.callback); } catch (e) { this.sendTo(msg.from, msg.command, { error: e.message }, msg.callback); } } } break; case 'getUsers': { if (msg.callback) { try { if (((_a = msg.message) === null || _a === void 0 ? void 0 : _a.ip) || this.config.fritzbox) { const users = await (0, fritzbox_1.getFritzBoxUsers)(((_b = msg.message) === null || _b === void 0 ? void 0 : _b.ip) || this.config.fritzbox); this.sendTo(msg.from, msg.command, users, msg.callback); } else { this.sendTo(msg.from, msg.command, [], msg.callback); } } catch (e) { this.sendTo(msg.from, msg.command, { error: e.message }, msg.callback); } } break; } case 'getFilter': { if (msg.callback) { try { if (((_c = msg.message) === null || _c === void 0 ? void 0 : _c.ip) || (this.config.fritzbox && ((_d = msg.message) === null || _d === void 0 ? void 0 : _d.login)) || (this.config.login && ((_e = msg.message) === null || _e === void 0 ? void 0 : _e.password)) || this.config.password) { const filter = await (0, fritzbox_1.getFritzBoxFilter)(((_f = msg.message) === null || _f === void 0 ? void 0 : _f.ip) || this.config.fritzbox, ((_g = msg.message) === null || _g === void 0 ? void 0 : _g.login) || this.config.login, ((_h = msg.message) === null || _h === void 0 ? void 0 : _h.password) || this.config.password); this.sendTo(msg.from, msg.command, { text: filter ? this.language === 'de' ? 'Fritz!Box unterstützt Filter-Funktion' : 'Fritz!Box supports Filter-Feature' : this.language === 'de' ? 'Fritz!Box unterstützt Filter-Funktion nicht' : 'Fritz!Box does not support Filter-Feature', style: { color: filter ? 'green' : 'red', }, }, msg.callback); } else { this.sendTo(msg.from, msg.command, false, msg.callback); } } catch (e) { this.sendTo(msg.from, msg.command, { error: e.message }, msg.callback); } } break; } case 'getInterfaces': { if (msg.callback) { try { if (((_j = msg.message) === null || _j === void 0 ? void 0 : _j.ip) || (this.config.fritzbox && ((_k = msg.message) === null || _k === void 0 ? void 0 : _k.login)) || (this.config.login && ((_l = msg.message) === null || _l === void 0 ? void 0 : _l.password)) || this.config.password) { const ifaces = await (0, fritzbox_1.getFritzBoxInterfaces)(((_m = msg.message) === null || _m === void 0 ? void 0 : _m.ip) || this.config.fritzbox, (_o = msg.message) === null || _o === void 0 ? void 0 : _o.login, (_p = msg.message) === null || _p === void 0 ? void 0 : _p.password, ((_q = msg.message) === null || _q === void 0 ? void 0 : _q.login) === this.config.login && msg.message.password === this.config.password ? this.sid : undefined); const lan1 = ifaces === null || ifaces === void 0 ? void 0 : ifaces.find(i => i.label === '1-lan'); if (lan1) { lan1.label += ' (default)'; } const index = ifaces === null || ifaces === void 0 ? void 0 : ifaces.findIndex(it => it === lan1); // place lan1 on the first position if (ifaces && index && index !== -1) { ifaces.splice(0, 0, ifaces.splice(index, 1)[0]); } this.sendTo(msg.from, msg.command, ifaces, msg.callback); } else { this.sendTo(msg.from, msg.command, [], msg.callback); } } catch (e) { this.sendTo(msg.from, msg.command, { error: e.message }, msg.callback); } } break; } case 'getMacForIps': if (msg.callback) { try { const devices = msg.message; const result = await this.getMacForIps(devices); this.sendTo(msg.from, msg.command, { result }, msg.callback); } catch (e) { this.sendTo(msg.from, msg.command, { error: e.message }, msg.callback); } } break; } } } async analyseError(response) { if (response.status === 404) { if (this.language === 'de') { this.log.error(`Registrieren auf der kisshome-cloud nicht möglich: Unbekannte E-Mail-Adresse`); } else { this.log.error(`Cannot register on the kisshome-cloud: Unknown email address`); } } else if (response.status === 403) { if (this.language === 'de') { this.log.error(`Registrieren auf der kisshome-cloud nicht möglich: Der öffentliche Schlüssel hat sich geändert. Bitte kontaktieren Sie uns unter kisshome@internet-sicherheit.de`); } else { this.log.error(`Cannot register on the kisshome-cloud: public key changed. Please contact us via kisshome@internet-sicherheit.de`); } await this.registerNotification('kisshome-research', 'publicKey', 'Public key changed'); } else if (response.status === 401) { if (this.language === 'de') { this.log.error(`Registrieren auf der kisshome-cloud nicht möglich: Ungültiges Passwort`); } else { this.log.error(`Cannot register on the cloud: invalid password`); } } else if (response.status === 422) { if (this.language === 'de') { this.log.error(`Registrieren auf der kisshome-cloud nicht möglich: E-Mail, öffentlicher Schlüssel oder UUID fehlen`); } else { this.log.error(`Cannot register on the cloud: missing email, public key or uuid`); } } else { if (this.language === 'de') { this.log.error(`Registrieren auf der kisshome-cloud nicht möglich: ${response.data || response.statusText || response.status}`); } else { this.log.error(`Cannot register on the kisshome-cloud: ${response.data || response.statusText || response.status}`); } } } async onReady() { var _a, _b, _c, _d, _e, _f; const date = new Date(); if (date.getFullYear() >= 2025 && date.getMonth() >= 4) { if (this.language === 'de') { this.log.error('Die Studie ist beendet. Danke für eure Teilnahme. Der Adapter beendet sich jetzt.'); } else { this.log.error('The study is finished. Thank you for your participation. The adapter will terminate now.'); } const obj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`); if (obj === null || obj === void 0 ? void 0 : obj.common) { obj.common.enabled = false; await this.setForeignObject(obj._id, obj); } return; } // read UUID const uuidObj = await this.getForeignObjectAsync('system.meta.uuid'); if ((_a = uuidObj === null || uuidObj === void 0 ? void 0 : uuidObj.native) === null || _a === void 0 ? void 0 : _a.uuid) { this.uuid = uuidObj.native.uuid; } else { if (this.language === 'de') { this.log.error('Kann UUID nicht auslesen'); } else { this.log.error('Cannot read UUID'); } return; } // remove running flag const runningState = await this.getStateAsync('info.connection'); if (runningState === null || runningState === void 0 ? void 0 : runningState.val) { await this.setState('info.connection', false, true); await this.setState('info.recording.running', false, true); } const captured = await this.getStateAsync('info.recording.captured'); if (captured === null || captured === void 0 ? void 0 : captured.val) { await this.setState('info.recording.captured', 0, true); } if (!this.config.fritzbox) { if (this.language === 'de') { this.log.error(`Fritz!Box is nicht eingegeben`); } else { this.log.error(`Fritz!Box is not defined`); } return; } // try to get MAC addresses for all IPs this.IPs = this.config.devices.filter(item => item.enabled && (item.ip || item.mac) && item.ip !== this.config.fritzbox); const tasks = this.IPs.filter(ip => !ip.mac); let fritzMac = ''; try { // determine the MAC of Fritzbox const fritzEntry = await this.getMacForIps([ { ip: this.config.fritzbox, mac: '', enabled: true, desc: 'FritzBox', uuid: '1' }, ]); fritzMac = ((_b = fritzEntry[0]) === null || _b === void 0 ? void 0 : _b.mac) || ''; } catch { if (this.language === 'de') { this.log.debug(`Kann die MAC Adresse von FritzBox nicht finden`); } else { this.log.debug(`Cannot determine MAC addresses of Fritz!Box`); } } if (tasks.length) { try { const macs = await this.getMacForIps(tasks); for (let i = 0; i < tasks.length; i++) { const mac = macs[i]; if (mac === null || mac === void 0 ? void 0 : mac.mac) { const item = this.IPs.find(t => t.ip === mac.ip); if (item) { item.mac = mac.mac; } } } // print out the IP addresses without MAC addresses const missing = this.IPs.filter(item => !item.mac); if (missing.length) { if (this.language === 'de') { this.log.warn(`Für folgende IP konnten keine MAC Adressen gefunden werden: ${missing.map(t => t.ip).join(', ')}`); } else { this.log.warn(`Cannot get MAC addresses for the following IPs: ${missing.map(t => t.ip).join(', ')}`); } } } catch (e) { if (e.toString().includes('no results')) { if (this.language === 'de') { this.log.warn(`Für folgende IP könnten keine MAC Adressen gefunden: ${tasks.map(t => t.ip).join(', ')}`); } else { this.log.warn(`Cannot get MAC addresses for the following IPs: ${tasks.map(t => t.ip).join(', ')}`); } } else { if (this.language === 'de') { this.log.error(`MAC-Adressen können nicht ermittelt werden: ${e}`); } else { this.log.error(`Cannot get MAC addresses: ${e}`); } } } } // take only unique MAC addresses and not MAC of Fritzbox this.uniqueMacs = []; this.IPs.forEach(item => { var _a; return !this.uniqueMacs.includes(item.mac) && ((_a = item.mac) === null || _a === void 0 ? void 0 : _a.trim()) && item.mac !== fritzMac && this.uniqueMacs.push(item.mac); }); this.uniqueMacs = this.uniqueMacs.filter(mac => mac); // detect temp directory this.tempDir = this.config.tempDir || '/run/shm'; if ((0, node_fs_1.existsSync)(this.tempDir)) { if (this.language === 'de') { this.log.info(`${this.tempDir} wird als temporäres Verzeichnis verwendet`); } else { this.log.info(`Using ${this.tempDir} as temporary directory`); } } else if ((0, node_fs_1.existsSync)('/run/shm')) { this.tempDir = '/run/shm'; if (this.language === 'de') { this.log.info(`${this.tempDir} wird als temporäres Verzeichnis verwendet`); } else { this.log.info(`Using ${this.tempDir} as temporary directory`); } } else if ((0, node_fs_1.existsSync)('/tmp')) { this.tempDir = '/tmp'; if (this.language === 'de') { this.log.info(`${this.tempDir} wird als temporäres Verzeichnis verwendet`); } else { this.log.info(`Using ${this.tempDir} as temporary directory`); } } else { if (this.language === 'de') { this.log.warn(`Es kann kein temporäres Verzeichnis gefunden werden. Bitte geben Sie es manuell in der Konfiguration an. Für beste Leistung sollte es eine RAM-Disk sein.`); } else { this.log.warn(`Cannot find any temporary directory. Please specify manually in the configuration. For best performance it should be a RAM disk`); } return; } this.tempDir = this.tempDir.replace(/\\/g, '/'); if (this.tempDir.endsWith('/')) { this.tempDir = this.tempDir.substring(0, this.tempDir.length - 1); } let privateKey = ''; // retrieve public and private keys let keysObj; try { keysObj = (await this.getObjectAsync('info.sync.keys')); if (!keysObj) { // try to migrate configuration keysObj = (await this.getObjectAsync('info.keys')); if (keysObj) { await this.setObjectAsync('info.sync.keys', keysObj); await this.delObjectAsync('info.keys'); } } } catch { // ignore keysObj = null; } if (!keysObj || !((_c = keysObj === null || keysObj === void 0 ? void 0 : keysObj.native) === null || _c === void 0 ? void 0 : _c.publicKey) || !((_d = keysObj.native) === null || _d === void 0 ? void 0 : _d.privateKey)) { // try to read the key on the address '0_userdata.0.kisshomeResearchPublicKey' let keysRestored = false; try { const keysState = await this.getForeignStateAsync(BACKUP_KEYS); if ((keysState === null || keysState === void 0 ? void 0 : keysState.val) && typeof keysState.val === 'string' && keysState.val.includes('/////')) { const [_public, _private] = keysState.val.split('/////'); this.publicKey = _public; privateKey = _private; keysRestored = true; if (this.language === 'de') { this.log.info('Schlüssel wurde aus "0_userdata.0.kisshomeResearchKeys" wiederherstellt.'); } else { this.log.info('The keys were restored from "0_userdata.0.kisshomeResearchKeys".'); } } } catch { // ignore } if (!keysRestored) { if (this.language === 'de') { this.log.info('Schlüssel werden erstmalig generiert.'); } else { this.log.info('Generating keys for the first time.'); } const result = (0, utils_1.generateKeys)(); privateKey = result.privateKey; this.publicKey = result.publicKey; } keysObj = { _id: 'info.sync.keys', type: 'config', common: { name: { en: 'Public and private keys', de: 'öffentliche und private Schlüssel', ru: 'Публичные и частные ключи', pt: 'Chaves públicas e privadas', nl: 'Openbare en privésleutels', fr: 'Clés publiques et privées', it: 'Chiavi pubbliche e private', es: 'Claves públicas y privadas', pl: 'Klucze publiczne i prywatne', uk: 'Публічні та приватні ключі', 'zh-cn': '公钥和私钥', }, }, native: { publicKey: this.publicKey, privateKey, }, }; await this.setObjectAsync(keysObj._id, keysObj); if (!keysRestored) { await this.saveKeyForUninstallAndInstall(this.publicKey, privateKey); } } else { privateKey = keysObj.native.privateKey; this.publicKey = keysObj.native.publicKey; await this.saveKeyForUninstallAndInstall(this.publicKey, privateKey, true); } if (!this.publicKey || !privateKey) { if (this.language === 'de') { this.log.error('Schlüssel können nicht generiert werden.'); } else { this.log.error('Cannot generate keys.'); } return; } this.workingDir = `${this.tempDir}/hourly_pcaps`; // create hourly directory try { if (!(0, node_fs_1.existsSync)(this.workingDir)) { (0, node_fs_1.mkdirSync)(this.workingDir); } } catch (e) { if (this.language === 'de') { this.log.error(`Arbeitsverzeichnis "${this.workingDir}" kann nicht erstellt werden: ${e}`); } else { this.log.error(`Cannot create working directory "${this.workingDir}": ${e}`); } return; } // this.clearWorkingDir(); if (!this.config.email) { if (this.language === 'de') { this.log.error('Keine E-Mail angegeben. Bitte geben Sie eine E-Mail-Adresse in der Konfiguration an.'); this.log.error('Sie müssen diese E-Mail zuerst unter https://kisshome-feldversuch.if-is.net/#register registrieren.'); } else { this.log.error('No email provided. Please provide an email address in the configuration.'); this.log.error('You must register this email first on https://kisshome-feldversuch.if-is.net/#register.'); } return; } try { // register on the cloud const response = await axios_1.default.post(`https://${PCAP_HOST}/api/v1/registerKey`, { publicKey: this.publicKey, email: this.config.email, uuid: this.uuid, }); if (response.status === 200) { if (((_e = response.data) === null || _e === void 0 ? void 0 : _e.command) === 'terminate') { if (this.language === 'de') { this.log.warn('Server hat die Terminierung des Adapters angefordert'); } else { this.log.warn('Server requested to terminate the adapter'); } const obj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`); if ((_f = obj === null || obj === void 0 ? void 0 : obj.common) === null || _f === void 0 ? void 0 : _f.enabled) { obj.common.enabled = false; await this.setForeignObjectAsync(obj._id, obj); } } else { if (this.language === 'de') { this.log.info('Erfolgreich in der Cloud registriert'); } else { this.log.info('Successfully registered on the cloud'); } } } else { await this.analyseError(response); return; } } catch (e) { if (e.response) { await this.analyseError(e.response); } else { if (this.language === 'de') { this.log.error(`Registrieren auf der kisshome-cloud nicht möglich: ${e}`); } else { this.log.error(`Cannot register on the kisshome-cloud: ${e}`); } } return; } this.saveMetaFile(); await this.setState('info.recording.running', false, true); await this.setState('info.recording.triggerWrite', false, true); if (!this.uniqueMacs.length) { if (this.language === 'de') { this.log.warn(`[PCAP] Keine MAC-Adressen für die Aufzeichnung angegeben. Bitte geben Sie einige MAC-Adressen oder IP-Adressen an, die in MAC-Adressen aufgelöst werden können`); } else { this.log.warn(`[PCAP] No any MAC addresses provided for recording. Please provide some MAC addresses or Ip addresses, that could be resolved to MAC address`); } return; } this.subscribeStates('info.recording.enabled'); this.subscribeStates('info.recording.triggerWrite'); this.recordingEnabled = ((await this.getStateAsync('info.recording.enabled')) || {}).val || false; if (this.recordingEnabled) { // start the monitoring this.startRecording().catch(e => { if (this.language === 'de') { this.log.error(`[PCAP] Aufzeichnen kann nicht gestartet werden: ${e}`); } else { this.log.error(`[PCAP] Cannot start recording: ${e}`); } }); // Send the data every hour to the cloud this.syncJob(); } else { if (this.language === 'de') { this.log.warn('Aufzeichnen ist nicht aktiviert. Nichts passiert.'); } else { this.log.warn('Recording is not enabled. Do nothing.'); } } } async saveKeyForUninstallAndInstall(publicKey, privateKey, check) { if (check) { // check if the key is already saved const keysState = await this.getForeignStateAsync(BACKUP_KEYS); if ((keysState === null || keysState === void 0 ? void 0 : keysState.val) === `${publicKey}/////${privateKey}`) { return; } if (keysState) { await this.setForeignStateAsync(BACKUP_KEYS, `${publicKey}/////${privateKey}`, true); return; } } // create state "0_userdata.0.kisshomeResearchKeys" await this.setForeignObjectAsync(BACKUP_KEYS, { type: 'state', common: { name: { en: 'Keys for KISSHome adapter', de: 'Schlüssel für KISSHome adapter', ru: 'Ключи для адаптера KISSHome', pt: 'Chaves para o adaptador KISSHome', nl: 'Sleutels voor KISSHome-adapter', fr: "Clés pour l'adaptateur KISSHome", it: "Chiavi per l'adattatore KISSHome", es: 'Claves para el adaptador KISSHome', pl: 'Klucze dla adaptera KISSHome', uk: 'Ключі для адаптера KISSHome', 'zh-cn': 'KISSHome适配器的密钥', }, desc: { en: 'It can be deleted if KISSHome adapter uninstalled and does not used anymore', de: 'Es kann gelöscht werden, wenn der KISSHome-Adapter deinstalliert und nicht mehr verwendet wird', ru: 'Его можно удалить, если адаптер KISSHome удален и больше не используется', pt: 'Pode ser excluído se o adaptador KISSHome for desinstalado e não for mais usado', nl: 'Het kan worden verwijderd als de KISSHome-adapter is verwijderd en niet meer wordt gebruikt', fr: "Il peut être supprimé si l'adaptateur KISSHome est désinstallé et n'est plus utilisé", it: "Può essere eliminato se l'adattatore KISSHome è disinstallato e non viene più utilizzato", es: 'Se puede eliminar si se desinstala el adaptador KISSHome y ya no se usa', pl: 'Można go usunąć, jeśli adapter KISSHome jest odinstalowany i nie jest już używany', uk: 'Можна видалити, якщо адаптер KISSHome видалено і більше не використовується', 'zh-cn': '如果KISSHome适配器已卸载且不再使用,则可以删除它', }, type: 'string', read: true, write: false, role: 'state', }, native: {}, }); await this.setForeignStateAsync(BACKUP_KEYS, `${publicKey}/////${privateKey}`, true); } syncJob() { // Send the data every hour to the cloud if (this.syncTimer) { clearTimeout(this.syncTimer); this.syncTimer = null; } if (this.context.terminate) { return; } const started = Date.now(); void this.startSynchronization() .catch(e => { if (this.language === 'de') { this.log.error(`[RSYNC] Kann nicht synchronisieren: ${e}`); } else { this.log.error(`[RSYNC] Cannot synchronize: ${e}`); } }) .then(() => { const duration = Date.now() - started; this.syncTimer = setTimeout(() => { this.syncTimer = null; this.syncJob(); }, SYNC_INTERVAL - duration > 0 ? SYNC_INTERVAL - duration : 0); }); } onStateChange(id, state) { if (state) { if (id === `${this.namespace}.info.recording.enabled` && !state.ack) { if (state.val) { // If recording is not running if (!this.recordingEnabled) { this.recordingEnabled = true; this.context.terminate = false; this.startRecording().catch(e => { if (this.language === 'de') { this.log.error(`Aufzeichnen kann nicht gestartet werden: ${e}`); } else { this.log.error(`Cannot start recording: ${e}`); } }); } } else if (this.recordingEnabled) { this.recordingEnabled = false; this.context.terminate = true; if (this.context.controller) { this.context.controller.abort(); this.context.controller = null; } } } else if (id === `${this.namespace}.info.recording.triggerWrite` && !state.ack) { if (state.val) { if (this.recordingRunning) { void this.setState('info.recording.triggerWrite', false, true).catch(e => this.language === 'de' ? this.log.error(`Kann triggerWrite nicht setzten: ${e}`) : this.log.error(`Cannot set triggerWrite: ${e}`)); this.savePacketsToFile(); setTimeout(() => { this.startSynchronization().catch(e => { if (this.language === 'de') { this.log.error(`[RSYNC] Kann nicht synchronisieren: ${e}`); } else { this.log.error(`[RSYNC] Cannot synchronize: ${e}`); } }); }, 2000); } } } } } restartRecording() { this.startTimeout && clearTimeout(this.startTimeout); this.startTimeout = this.setTimeout(() => { this.startTimeout = undefined; this.startRecording().catch(e => { if (this.language === 'de') { this.log.error(`Aufzeichnen kann nicht gestartet werden: ${e}`); } else { this.log.error(`Cannot start recording: ${e}`); } }); }, 10000); } savePacketsToFile() { if (this.context.packets.length) { const packetsToSave = this.context.packets; this.context.packets = []; this.context.totalBytes = 0; const timeStamp = KISSHomeResearchAdapter.getTimestamp(); const fileName = `${this.workingDir}/${timeStamp}.pcap`; // get file descriptor of a file const fd = (0, node_fs_1.openSync)(fileName, 'w'); let offset = 0; const magic = packetsToSave[0].readUInt32LE(0); const STANDARD_MAGIC = 0xa1b2c3d4; // https://wiki.wireshark.org/Development/LibpcapFileFormat const MODIFIED_MAGIC = 0xa1b2cd34; // do not save a header if it is already present // write header if (magic !== STANDARD_MAGIC && magic !== MODIFIED_MAGIC) { // create PCAP header const byteArray = Buffer.alloc(6 * 4); // magic number byteArray.writeUInt32LE(this.context.modifiedMagic || this.context.libpCapFormat ? MODIFIED_MAGIC : STANDARD_MAGIC, 0); // major version byteArray.writeUInt16LE(2, 4); // minor version byteArray.writeUInt16LE(4, 6); // reserved byteArray.writeUInt32LE(0, 8); // reserved byteArray.writeUInt32LE(0, 12); // SnapLen byteArray.writeUInt16LE(recording_1.MAX_PACKET_LENGTH, 16); // network type byteArray.writeUInt32LE(this.context.networkType, 20); (0, node_fs_1.writeSync)(fd, byteArray, 0, byteArray.length, 0); offset = byteArray.length; } for (let i = 0; i < packetsToSave.length; i++) { const packet = packetsToSave[i]; (0, node_fs_1.writeSync)(fd, packet, 0, packet.length, offset); offset += packet.length; } (0, node_fs_1.closeSync)(fd); if (this.language === 'de') { this.log.debug(`Datei ${fileName} mit ${size2text(offset)} gespeichert`); } else { this.log.debug(`Saved file ${fileName} with ${size2text(offset)}`); } } this.context.lastSaved = Date.now(); } calculateMd5(content) { const hash = (0, node_crypto_1.createHash)('md5'); hash.update(content); return hash.digest('hex'); } async startRecording() { if (!this.uniqueMacs.length) { if (this.language === 'de') { this.log.error(`[PCAP] Keine MAC-Adressen für die Aufzeichnung angegeben. Bitte geben Sie einige MAC-Adressen oder IP-Adressen an, die in MAC-Adressen aufgelöst werden können`); } else { this.log.error(`[PCAP] No any MAC addresses provided for recording. Please provide some MAC addresses or Ip addresses, that could be resolved to MAC address`); } return; } // take sid from fritzbox if (!this.sid || !this.sidCreated || Date.now() - this.sidCreated >= 3600000) { try { this.sid = (await (0, fritzbox_1.getFritzBoxToken)(this.config.fritzbox, this.config.login, this.config.password, (text) => this.log.warn(text))) || ''; this.sidCreated = Date.now(); } catch (e) { this.sid = ''; this.sidCreated = 0; if (this.language === 'de') { this.log.error(`[PCAP] SID kann nicht von Fritz!Box abgerufen werden: ${e}`); } else { this.log.error(`[PCAP] Cannot get SID from Fritz!Box: ${e}`); } } } if (this.sid) { if (this.language === 'de') { this.log.debug(`[PCAP] Nutze SID: ${this.sid}`); } else { this.log.debug(`[PCAP] Use SID: ${this.sid}`); } const captured = await this.getStateAsync('info.recording.captured'); if (captured === null || captured === void 0 ? void 0 : captured.val) { await this.setState('info.recording.captured', 0, true); } this.context.controller = new AbortController(); this.context.packets = []; this.context.totalBytes = 0; this.context.totalPackets = 0; this.context.lastSaved = Date.now(); // stop all recordings const response = await (0, recording_1.stopAllRecordingsOnFritzBox)(this.config.fritzbox, this.sid); if (response) { if (this.language === 'de') { this.log.info(`[PCAP] Stopped all recordings on Fritz!Box: ${response}`); } else { this.log.info(`[PCAP] Alle Aufnahmen auf der Fritz!Box wurden beendet: ${response}`); } } if (this.language === 'de') { this.log.debug(`[PCAP] Starte das Mitschneiden von ${this.config.fritzbox}/"${this.config.iface}"...`); } else { this.log.debug(`[PCAP] Starting recording on ${this.config.fritzbox}/"${this.config.iface}"...`); } this.log.debug(`[PCAP] ${(0, recording_1.getRecordURL)(this.config.fritzbox, this.sid, this.config.iface, this.uniqueMacs)}`); (0, recording_1.startRecordingOnFritzBox)(this.config.fritzbox, this.sid, this.config.iface, this.uniqueMacs, async (error) => { var _a; this.monitorInterval && this.clearInterval(this.monitorInterval); this.monitorInterval = undefined; this.savePacketsToFile(); this.context.totalBytes = 0; this.context.totalPackets = 0; if ((error === null || error === void 0 ? void 0 : error.message) === 'Unauthorized') { this.sid = ''; this.sidCreated = 0; } if (this.recordingRunning) { this.log.info(`[PCAP] Recording stopped.`); this.log.info(`[PCAP] Mitschneiden beendet.`); this.recordingRunning = false; await this.setState('info.connection', false, true); await this.setState('info.recording.running', false, true); } if ((_a = this.context.packets) === null || _a === void 0 ? void 0 : _a.length) { await this.setState('info.recording.captured', this.context.totalPackets, true); } if (error) { if (!this.context.terminate || !error.toString().includes('aborted')) { this.log.error(`[PCAP] Error while recording: ${error.toString()}`); this.log.error(`[PCAP] Fehler wehrend dem Mitschneiden: ${error.toString()}`); } } if (!this.context.terminate) { this.restartRecording(); } }, this.context, async () => { if (!this.recordingRunning) { this.log.debug('[PCAP] Recording started!'); this.log.debug('[PCAP] Aufzeichnen hat gestartet!'); this.recordingRunning = true; await this.setState('info.connection', true, true); await this.setState('info.recording.running', true, true); this.monitorInterval = this.monitorInterval || this.setInterval(() => { if (Date.now() - this.lastDebug > 60000) { this.log.debug(`[PCAP] Captured ${this.context.totalPackets} packets (${size2text(this.context.totalBytes)})`); this.log.debug(`[PCAP] ${this.context.totalPackets} Pakete (${size2text(this.context.totalBytes)}) aufgezeichnet`); this.lastDebug = Date.now(); } // save if a file is bigger than 50 Mb if (this.context.totalBytes > SAVE_DATA_IF_BIGGER || // save every 20 minutes Date.now() - this.context.lastSaved >= SAVE_DATA_EVERY_MS) { this.savePacketsToFile(); if (!this.context.terminate) { this.startSynchronization().catch(e => { this.log.error(`[RSYNC] Cannot synchronize: ${e}`); this.log.error(`[RSYNC] Kann nicht synchronisieren: ${e}`); }); } } }, 10000); } await this.setState('info.recording.captured', this.context.totalPackets, true); }, (text, level = 'info') => { this.log[level](`[PCAP] ${text}`); }); } else { if (this.language === 'de') { this.log.warn('[PCAP] Anmelden auf Fritz!Box nicht möglich. Vermutlich falsche Anmeldedaten oder die Fritz!Box ist nicht verfügbar.'); } else { this.log.warn('[PCAP] Cannot login into Fritz!Box. Could be wrong credentials or Fritz!Box is not available'); } // try to get the token in 10 seconds again. E.g., if fritzbox is rebooting this.restartRecording(); } } static getTimestamp() { const now = new Date(); return `${now.getUTCFullYear()}-${(now.getUTCMonth() + 1).toString().padStart(2, '0')}-${now.getUTCDate().toString().padStart(2, '0')}_${now.getUTCHours().toString().padStart(2, '0')}-${now.getUTCMinutes().toString().padStart(2, '0')}-${now.getUTCSeconds().toString().padStart(2, '0')}`; } saveMetaFile() { const text = KISSHomeResearchAdapter.getDescriptionFile(this.IPs); const newFile = `${this.workingDir}/${KISSHomeResearchAdapter.getTimestamp()}_v${this.versionPack}_meta.json`; try { // find the latest file let changed = false; let files = (0, node_fs_1.readdirSync)(this.workingDir); // sort descending files.sort((a, b) => b.localeCompare(a)); // if two JSON files are coming after each other, the older one must be deleted for (let f = files.length - 1; f > 0; f--) { if (files[f].endsWith('_meta.json') && files[f - 1].endsWith('_meta.json')) { (0, node_fs_1.unlinkSync)(`${this.workingDir}/${files[f]}`); changed = true; } } // read the list anew as it was changed if (changed) { files = (0, node_fs_1.readdirSync)(this.workingDir); // sort descending files.sort((a, b) => b.localeCompare(a)); } // find the latest file and delete all other _meta.json files const latestFile = files.find(f => f.endsWith('_meta.json')); // if existing meta file found if (latestFile) { // compare the content const oldFile = (0, node_fs_1.readFileSync)(`${this.workingDir}/${latestFile}`, 'utf8'); if (oldFile !== text) { if (this.language === 'de') { this.log.debug('Meta-Datei aktualisiert'); } else { this.log.debug('Meta file updated'); } // delete the old JSON file only if no pcap files exists if (files[0].endsWith('_meta.json')) { (0, node_fs_1.unlinkSync)(`${this.workingDir}/${latestFile}`); } (0, node_fs_1.writeFileSync)(newFile, text); return newFile; } return `${this.workingDir}/${latestFile}`; } if (this.language === 'de') { this.log.info('Meta-Datei wurde angelegt.'); } else { this.log.info('Meta file created'); } // if not found => create new one (0, node_fs_1.writeFileSync)(newFile, text); return newFile; } catch (e) { if (this.language === 'de') { this.log.warn(`Speicher von Meta-Datei "${newFile}" nicht möglich: ${e}`); } else { this.log.warn(`Cannot save meta file "${newFile}": ${e}`); } return ''; } } static getDescriptionFile(IPs) { const desc = {}; IPs.sort((a, b) => a.ip.localeCompare(b.ip)).forEach(ip => { if (ip.mac) { desc[ip.mac] = { ip: ip.ip, desc: ip.desc }; } }); return JSON.stringify(desc, null, 2); } async getMacForIps(devices) { const result = []; let error = ''; for (const dev of devices) { if (dev.ip && KISSHomeResearchAdapter.macCache[dev.ip]) { result.push({ ...KISSHomeResearchAdapter.macCache[dev.ip], ip: dev.ip, found: true }); continue; } if (!dev.mac && dev.ip && (0, utils_1.validateIpAddress)(dev.ip)) { try { const mac = await (0, utils_1.getMacForIp)(dev.ip); if (mac) { result.push({ ...mac, found: true }); KISSHomeResearchAdapter.macCache[dev.ip] = { mac: mac.mac, vendor: mac.vendor }; } else { if (this.language === 'de') { this.log.warn(`Kann die MAC Adresse von ${dev.ip} nicht auflösen`); } else { this.log.warn(`Cannot resolve MAC address of ${dev.ip}`); } } } catch (e) { error = e.message; } } else { const item = { mac: dev.mac, ip: dev.ip, vendor: dev.mac ? (0, utils_1.getVendorForMac)(dev.mac) : '', found: false, }; result.push(item); } } if (!result.length && devices.length) { throw new Error(error || 'no results'); } return result; } async onUnload(callback) { this.context.terminate = true; if (this.recordingRunning) { this.recordingRunning = false; await this.setState('info.connection', false, true); await this.setState('info.recording.running', false, true); } if (this.syncTimer) { clearTimeout(this.syncTimer); this.syncTimer = null; }