iobroker.kisshome-defender
Version:
Collection of information for KISSHome defender
380 lines • 18 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PCAP_HOST = void 0;
const adapter_core_1 = require("@iobroker/adapter-core");
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const node_crypto_1 = require("node:crypto");
const axios_1 = __importDefault(require("axios"));
const utils_1 = require("./utils");
const SYNC_INTERVAL = 3600000; // 1 hour;
exports.PCAP_HOST = 'kisshome-experiments.if-is.net';
class CloudSync {
constructor(adapter, options) {
this.syncRunning = false;
this.syncTimer = null;
this.emailOk = null;
this.justSending = '';
this.collectUxEvents = null;
this.timeoutUxEvents = null;
this.version = options.version;
this.adapter = adapter;
this.config = this.adapter.config;
this.workingDir = options.workingDir;
this.context = options.context;
this.uuid = options.uuid;
this.IPs = options.IPs;
this.saveMetaFile();
this.ready = new Promise(resolve => this.init(resolve));
}
async analyseError(response) {
if (response.status === 404) {
this.adapter.log.error(`${adapter_core_1.I18n.translate('Cannot register on the kisshome-cloud')}: ${adapter_core_1.I18n.translate('Unknown email address')}`);
}
else if (response.status === 403) {
this.adapter.log.error(`${adapter_core_1.I18n.translate('Cannot register on the kisshome-cloud')}: ${adapter_core_1.I18n.translate('UUID changed. Please contact us via kisshome@internet-sicherheit.de')}`);
await this.adapter.registerNotification('kisshome-defender', 'uuid', 'UUID wurde geändert');
}
else if (response.status === 401) {
this.adapter.log.error(`${adapter_core_1.I18n.translate('Cannot register on the kisshome-cloud')}: ${adapter_core_1.I18n.translate('invalid password')}`);
}
else if (response.status === 422) {
this.adapter.log.error(`${adapter_core_1.I18n.translate('Cannot register on the kisshome-cloud')}: ${adapter_core_1.I18n.translate('missing email, public key or uuid')}`);
}
else {
this.adapter.log.error(`${adapter_core_1.I18n.translate('Cannot register on the kisshome-cloud')}: ${response.data || response.statusText || response.status}`);
}
}
async init(callback) {
var _a, _b;
try {
// register on the cloud
const response = await axios_1.default.post(`https://${exports.PCAP_HOST}/api/v2/checkEmail?email=${encodeURIComponent(this.config.email)}&uuid=${encodeURIComponent(this.uuid)}`, {
timeout: 10000, // 10-second timeout
});
if (response.status === 200) {
if (((_a = response.data) === null || _a === void 0 ? void 0 : _a.command) === 'terminate') {
this.adapter.log.warn(adapter_core_1.I18n.translate('Server requested to terminate the adapter'));
const obj = await this.adapter.getForeignObjectAsync(`system.adapter.${this.adapter.namespace}`);
if ((_b = obj === null || obj === void 0 ? void 0 : obj.common) === null || _b === void 0 ? void 0 : _b.enabled) {
obj.common.enabled = false;
await this.adapter.setForeignObjectAsync(obj._id, obj);
}
}
else {
this.emailOk = true;
this.adapter.log.info(adapter_core_1.I18n.translate('Successfully registered on the cloud'));
}
}
else {
this.emailOk = false;
await this.analyseError(response);
}
}
catch (error) {
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
// timeout: retry in 5 seconds
this.adapter.log.warn(`${adapter_core_1.I18n.translate('Cannot register on the kisshome-cloud')}: timeout`);
}
else if (error.response) {
this.emailOk = false;
await this.analyseError(error.response);
}
else {
this.emailOk = false;
this.adapter.log.error(`${adapter_core_1.I18n.translate('Cannot register on the kisshome-cloud')}: ${error}`);
}
}
if (this.emailOk === null && !this.context.terminate) {
setTimeout(() => {
void this.init(callback);
}, 5000);
}
else {
callback();
}
}
start() {
this.syncJob();
}
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.startCloudSynchronization()
.catch(e => {
this.adapter.log.error(`[RSYNC] ${adapter_core_1.I18n.translate('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);
});
}
stop() {
var _a;
if (this.syncTimer) {
clearTimeout(this.syncTimer);
this.syncTimer = null;
}
if (this.timeoutUxEvents) {
clearTimeout(this.timeoutUxEvents);
this.timeoutUxEvents = null;
}
if ((_a = this.collectUxEvents) === null || _a === void 0 ? void 0 : _a.length) {
this.saveUxEvents(this.collectUxEvents);
this.collectUxEvents = null;
}
}
async isEmailOk() {
await this.ready;
return this.emailOk;
}
static calculateMd5(content) {
const hash = (0, node_crypto_1.createHash)('md5');
hash.update(content);
return hash.digest('hex');
}
async sendOneFileToCloud(fileName, size) {
var _a, _b;
try {
if (!(0, node_fs_1.existsSync)(fileName)) {
this.adapter.log.warn(`[RSYNC] ${adapter_core_1.I18n.translate('File "%s" does not exist. Size: %s', fileName, size ? (0, utils_1.size2text)(size) : 'unknown')}`);
return;
}
const data = (0, node_fs_1.readFileSync)(fileName);
const name = (0, node_path_1.basename)(fileName);
const len = data.length;
const md5 = CloudSync.calculateMd5(data);
this.justSending = fileName;
// check if the file was sent successfully
try {
const responseCheck = await axios_1.default.get(`https://${exports.PCAP_HOST}/api/v2/upload/${encodeURIComponent(this.config.email)}/${encodeURIComponent(name)}?uuid=${encodeURIComponent(this.uuid)}`);
if (((_a = responseCheck.data) === null || _a === void 0 ? void 0 : _a.command) === 'terminate') {
const obj = await this.adapter.getForeignObjectAsync(`system.adapter.${this.adapter.namespace}`);
if ((_b = obj === null || obj === void 0 ? void 0 : obj.common) === null || _b === void 0 ? void 0 : _b.enabled) {
obj.common.enabled = false;
await this.adapter.setForeignObjectAsync(obj._id, obj);
}
return;
}
if (responseCheck.status === 200 && responseCheck.data === md5) {
// file already uploaded, do not upload it again
if (!name.endsWith('_meta.json')) {
this.justSending = '';
(0, node_fs_1.unlinkSync)(fileName);
}
return;
}
}
catch {
// ignore
}
const responsePost = await (0, axios_1.default)({
method: 'post',
url: `https://${exports.PCAP_HOST}/api/v2/upload/${encodeURIComponent(this.config.email)}/${encodeURIComponent(name)}?&uuid=${encodeURIComponent(this.uuid)}`,
data,
headers: { 'Content-Type': 'application/vnd.tcpdump.pcap' },
});
// check if the file was sent successfully
const response = await axios_1.default.get(`https://${exports.PCAP_HOST}/api/v2/upload/${encodeURIComponent(this.config.email)}/${encodeURIComponent(name)}?&uuid=${encodeURIComponent(this.uuid)}`);
if (response.status === 200 && response.data === md5) {
if (!name.endsWith('_meta.json')) {
(0, node_fs_1.unlinkSync)(fileName);
}
this.adapter.log.debug(`[RSYNC] ${adapter_core_1.I18n.translate('Sent file "%s"(%s) to the cloud', fileName, (0, utils_1.size2text)(len))} (${size ? (0, utils_1.size2text)(size) : adapter_core_1.I18n.translate('unknown')}): ${responsePost.status}`);
}
else {
this.adapter.log.warn(`[RSYNC] ${adapter_core_1.I18n.translate('File sent to server, but check fails (%s). "%s" to the cloud', size ? (0, utils_1.size2text)(size) : adapter_core_1.I18n.translate('unknown'), fileName)}: status=${responsePost.status}, len=${len}, response=${response.data}`);
}
}
catch (e) {
this.adapter.log.error(`[RSYNC] ${adapter_core_1.I18n.translate('Cannot send file "%s" to the cloud', fileName)} (${size ? (0, utils_1.size2text)(size) : adapter_core_1.I18n.translate('unknown')}): ${e}`);
}
}
saveMetaFile() {
var _a;
const text = (0, utils_1.getDescriptionFile)(this.IPs);
const newFile = `${this.workingDir}/${(0, utils_1.getTimestamp)()}_v${this.version}_${((_a = this.config.docker) === null || _a === void 0 ? void 0 : _a.selfHosted) ? 'auto' : 'manual'}_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) {
this.adapter.log.debug(adapter_core_1.I18n.translate('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}`;
}
this.adapter.log.info(adapter_core_1.I18n.translate('Meta file created'));
// if not found => create new one
(0, node_fs_1.writeFileSync)(newFile, text);
return newFile;
}
catch (e) {
this.adapter.log.warn(`${adapter_core_1.I18n.translate('Cannot save meta file "%s"', newFile)}: ${e}`);
return '';
}
}
reportUxEvents(uxEvents) {
this.collectUxEvents || (this.collectUxEvents = []);
this.collectUxEvents.push(...uxEvents);
this.timeoutUxEvents || (this.timeoutUxEvents = setTimeout(() => {
var _a;
this.timeoutUxEvents = null;
if ((_a = this.collectUxEvents) === null || _a === void 0 ? void 0 : _a.length) {
this.saveUxEvents(this.collectUxEvents);
this.collectUxEvents = null;
}
}, 120000));
}
saveUxEvents(uxEvents) {
// Find UX events files
let fileName;
const files = (0, node_fs_1.readdirSync)(this.workingDir)
.filter(f => f.includes('ux_events') && f.endsWith('.json'))
.sort();
if (!files.length || files[files.length - 1] === this.justSending) {
// create a new file
fileName = `${this.workingDir}/${(0, utils_1.getTimestamp)()}_ux_events.json`;
}
else {
// use the last file
fileName = `${this.workingDir}/${files[files.length - 1]}`;
}
let existingEvents = [];
if ((0, node_fs_1.existsSync)(fileName)) {
try {
existingEvents = JSON.parse((0, node_fs_1.readFileSync)(fileName, 'utf8'));
}
catch (e) {
this.adapter.log.warn(`${adapter_core_1.I18n.translate('Cannot read UX events file "%s"', fileName)}: ${e}`);
}
}
existingEvents.push(...uxEvents);
// save the file
try {
(0, node_fs_1.writeFileSync)(fileName, JSON.stringify(existingEvents, null, 2), 'utf8');
this.adapter.log.debug(`[RSYNC] ${adapter_core_1.I18n.translate('Saved UX events to file "%s"', fileName)} (${(0, utils_1.size2text)(Buffer.byteLength(JSON.stringify(existingEvents, null, 2)))})`);
}
catch (e) {
this.adapter.log.warn(`${adapter_core_1.I18n.translate('Cannot save UX events file "%s"', fileName)}: ${e}`);
}
}
async startCloudSynchronization() {
var _a;
await this.ready;
if (this.context.terminate) {
this.adapter.log.debug(`[RSYNC] ${adapter_core_1.I18n.translate('Requested termination. No synchronization')}`);
return;
}
if (!this.emailOk) {
this.adapter.log.warn(`[RSYNC] ${adapter_core_1.I18n.translate('Email not registered. No synchronization')}`);
return;
}
// if UX events are collected, save them
if ((_a = this.collectUxEvents) === null || _a === void 0 ? void 0 : _a.length) {
this.saveUxEvents(this.collectUxEvents);
this.collectUxEvents = null;
}
// calculate the total number of bytes
let totalBytes = 0;
this.adapter.log.debug(`[RSYNC] ${adapter_core_1.I18n.translate('Start synchronization...')}`);
// calculate the total number of bytes in pcap files
let pcapFiles;
let allFiles;
const sizes = {};
try {
allFiles = (0, node_fs_1.readdirSync)(this.workingDir);
pcapFiles = allFiles.filter(f => f.endsWith('.pcap'));
for (const file of pcapFiles) {
sizes[file] = (0, node_fs_1.statSync)(`${this.workingDir}/${file}`).size;
totalBytes += sizes[file];
}
}
catch (e) {
this.adapter.log.error(`[RSYNC] ${adapter_core_1.I18n.translate('Cannot read working directory "%s" for sync', this.workingDir)}: ${e}`);
return;
}
if (!totalBytes) {
this.adapter.log.debug(`[RSYNC] ${adapter_core_1.I18n.translate('No files to sync')}`);
return;
}
if (this.syncRunning) {
this.adapter.log.warn(`[RSYNC] ${adapter_core_1.I18n.translate('Synchronization still running...')}`);
return;
}
this.syncRunning = true;
await this.adapter.setState('info.cloudSync.running', true, true);
this.adapter.log.debug(`[RSYNC] ${adapter_core_1.I18n.translate('Syncing files to the cloud')} (${(0, utils_1.size2text)(totalBytes)})`);
// send files to the cloud
// first send JSON files and meta file
let sentMeta = false;
for (let i = 0; i < allFiles.length; i++) {
const file = allFiles[i];
if (file.endsWith('.json')) {
await this.sendOneFileToCloud(`${this.workingDir}/${file}`);
if (file.endsWith('_meta.json')) {
sentMeta = true;
}
}
}
if (!sentMeta) {
// create meta file anew and send it to the cloud
const fileName = this.saveMetaFile();
if (fileName) {
await this.sendOneFileToCloud(fileName);
}
else {
this.adapter.log.debug(`[RSYNC] ${adapter_core_1.I18n.translate('Cannot create META file. No synchronization')}`);
return;
}
}
// send all pcap files
for (let i = 0; i < pcapFiles.length; i++) {
const file = pcapFiles[i];
await this.sendOneFileToCloud(`${this.workingDir}/${file}`, sizes[file]);
}
this.syncRunning = false;
await this.adapter.setState('info.cloudSync.running', false, true);
}
}
exports.default = CloudSync;
//# sourceMappingURL=CloudSync.js.map