UNPKG

@iobroker/socket-classes

Version:
1,160 lines (1,159 loc) 98.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SocketCommands = exports.COMMANDS_PERMISSIONS = void 0; const adapter_core_1 = require("@iobroker/adapter-core"); // Get common adapter utils exports.COMMANDS_PERMISSIONS = { getObject: { type: 'object', operation: 'read' }, getObjects: { type: 'object', operation: 'list' }, getObjectView: { type: 'object', operation: 'list' }, setObject: { type: 'object', operation: 'write' }, requireLog: { type: 'object', operation: 'write' }, // just mapping to some command delObject: { type: 'object', operation: 'delete' }, extendObject: { type: 'object', operation: 'write' }, getHostByIp: { type: 'object', operation: 'list' }, subscribeObjects: { type: 'object', operation: 'read' }, unsubscribeObjects: { type: 'object', operation: 'read' }, getStates: { type: 'state', operation: 'list' }, getState: { type: 'state', operation: 'read' }, setState: { type: 'state', operation: 'write' }, delState: { type: 'state', operation: 'delete' }, createState: { type: 'state', operation: 'create' }, subscribe: { type: 'state', operation: 'read' }, unsubscribe: { type: 'state', operation: 'read' }, getStateHistory: { type: 'state', operation: 'read' }, getVersion: { type: '', operation: '' }, getAdapterName: { type: '', operation: '' }, addUser: { type: 'users', operation: 'create' }, delUser: { type: 'users', operation: 'delete' }, addGroup: { type: 'users', operation: 'create' }, delGroup: { type: 'users', operation: 'delete' }, changePassword: { type: 'users', operation: 'write' }, httpGet: { type: 'other', operation: 'http' }, cmdExec: { type: 'other', operation: 'execute' }, sendTo: { type: 'other', operation: 'sendto' }, sendToHost: { type: 'other', operation: 'sendto' }, readLogs: { type: 'other', operation: 'execute' }, readDir: { type: 'file', operation: 'list' }, createFile: { type: 'file', operation: 'create' }, writeFile: { type: 'file', operation: 'write' }, readFile: { type: 'file', operation: 'read' }, fileExists: { type: 'file', operation: 'read' }, deleteFile: { type: 'file', operation: 'delete' }, readFile64: { type: 'file', operation: 'read' }, writeFile64: { type: 'file', operation: 'write' }, unlink: { type: 'file', operation: 'delete' }, rename: { type: 'file', operation: 'write' }, mkdir: { type: 'file', operation: 'write' }, chmodFile: { type: 'file', operation: 'write' }, chownFile: { type: 'file', operation: 'write' }, subscribeFiles: { type: 'file', operation: 'read' }, unsubscribeFiles: { type: 'file', operation: 'read' }, authEnabled: { type: '', operation: '' }, disconnect: { type: '', operation: '' }, listPermissions: { type: '', operation: '' }, getUserPermissions: { type: 'object', operation: 'read' }, }; const pattern2RegEx = adapter_core_1.commonTools.pattern2RegEx; let axiosGet = null; let zipFiles = null; class SocketCommands { static ERROR_PERMISSION = 'permissionError'; static COMMANDS_PERMISSIONS = exports.COMMANDS_PERMISSIONS; adapter; context; commands = {}; subscribes = {}; #logEnabled = false; #clientSubscribes = {}; #updateSession; adapterName; _sendToHost; states; constructor(adapter, updateSession, context) { this.adapter = adapter; this.#updateSession = updateSession || (() => true); this.context = context || { language: 'en', ratings: null, ratingTimeout: null, }; // Do not initialize the context.language by admin, as admin could change the language if (adapter.name !== 'admin' && !context?.language && adapter?.getForeignObjectAsync) { void adapter.getForeignObjectAsync('system.config').then(obj => { if (obj?.common?.language) { this.context.language = obj.common.language; } }); } this._sendToHost = null; this.#initCommands(); } /** * Rename file or folder * * @param adapter Object ID * @param oldName Old file name * @param newName New file name * @param options options { user?: string; } */ async #rename(adapter, oldName, newName, options) { // read if it is a file or folder try { if (oldName.endsWith('/')) { oldName = oldName.substring(0, oldName.length - 1); } if (newName.endsWith('/')) { newName = newName.substring(0, newName.length - 1); } const files = await this.adapter.readDirAsync(adapter, oldName, options); if (files?.length) { for (let f = 0; f < files.length; f++) { await this.#rename(adapter, `${oldName}/${files[f].file}`, `${newName}/${files[f].file}`); } } } catch (error) { if (error.message !== 'Not exists') { throw error; } // else ignore, because it is a file and not a folder } try { await this.adapter.renameAsync(adapter, oldName, newName, options); } catch (error) { if (error.message !== 'Not exists') { throw error; } // else ignore, because the folder cannot be deleted } } /** * Delete file or folder * * @param adapter Object ID * @param name File name * @param options options { user?: string; } */ async #unlink(adapter, name, options) { // read if it is a file or folder try { // remove trailing '/' if (name.endsWith('/')) { name = name.substring(0, name.length - 1); } const files = await this.adapter.readDirAsync(adapter, name, options); if (files?.length) { for (let f = 0; f < files.length; f++) { await this.#unlink(adapter, `${name}/${files[f].file}`); } } } catch (error) { // ignore, because it is a file and not a folder if (error.message !== 'Not exists') { throw error; } } try { await this.adapter.unlinkAsync(adapter, name, options); } catch (error) { if (error.message !== 'Not exists') { throw error; } // else ignore, because folder cannot be deleted } } /** * Convert errors into strings and then call cb * * @param callback Callback function * @param error Error * @param args Arguments passed to callback */ static _fixCallback(callback, error, ...args) { if (typeof callback !== 'function') { return; } if (error instanceof Error) { error = error.message; } callback(error, ...args); } _checkPermissions(socket, command, callback, ...args) { const _command = command; if (socket._acl?.user !== 'system.user.admin') { // type: file, object, state, other // operation: create, read, write, list, delete, sendto, execute, sendToHost, readLogs if (SocketCommands.COMMANDS_PERMISSIONS[_command]) { // If permission required const commandType = SocketCommands.COMMANDS_PERMISSIONS[_command].type; if (commandType) { if (commandType === 'object') { const operation = SocketCommands.COMMANDS_PERMISSIONS[_command].operation; if (socket._acl?.object?.[operation]) { return true; } } else if (commandType === 'state') { const operation = SocketCommands.COMMANDS_PERMISSIONS[_command].operation; if (socket._acl?.state?.[operation]) { return true; } } else if (commandType === 'users') { const operation = SocketCommands.COMMANDS_PERMISSIONS[_command].operation; if (socket._acl?.users?.[operation]) { return true; } } else if (commandType === 'other') { const operation = SocketCommands.COMMANDS_PERMISSIONS[_command].operation; if (socket._acl?.other?.[operation]) { return true; } } else if (commandType === 'file') { const operation = SocketCommands.COMMANDS_PERMISSIONS[_command].operation; if (socket._acl?.file?.[operation]) { return true; } } this.adapter.log.warn(`No permission for "${socket._acl?.user}" to call ${_command}. Need "${commandType}"."${SocketCommands.COMMANDS_PERMISSIONS[_command].operation}"`); } else { return true; } } else { this.adapter.log.warn(`No rule for command: ${_command}`); } if (typeof callback === 'function') { callback(SocketCommands.ERROR_PERMISSION); } else { if (SocketCommands.COMMANDS_PERMISSIONS[_command]) { socket.emit(SocketCommands.ERROR_PERMISSION, { command, type: SocketCommands.COMMANDS_PERMISSIONS[_command].type, operation: SocketCommands.COMMANDS_PERMISSIONS[_command].operation, args, }); } else { socket.emit(SocketCommands.ERROR_PERMISSION, { command: _command, args }); } } return false; } return true; } publish(socket, type, id, obj) { if (socket?.subscribe?.[type] && this.#updateSession(socket)) { return !!socket.subscribe[type].find(sub => { if (sub.regex.test(id)) { // replace language if (this.context.language && id === 'system.config' && obj.common) { obj.common.language = this.context.language; } socket.emit(type, id, obj); return true; } }); } return false; } publishFile(socket, id, fileName, size) { if (socket?.subscribe?.fileChange && this.#updateSession(socket)) { const key = `${id}####${fileName}`; return !!socket.subscribe.fileChange.find(sub => { if (sub.regex.test(key)) { socket.emit('fileChange', id, fileName, size); return true; } }); } return false; } publishInstanceMessage(socket, sourceInstance, messageType, data) { if (this.#clientSubscribes[socket.id]?.[sourceInstance]?.includes(messageType)) { socket.emit('im', messageType, sourceInstance, data); return true; } // inform instance about missing subscription this.adapter.sendTo(sourceInstance, 'clientSubscribeError', { type: messageType, sid: socket.id, reason: 'no one subscribed', }); return false; } _showSubscribes(socket, type) { if (socket?.subscribe) { const s = socket.subscribe[type] || []; const ids = []; for (let i = 0; i < s.length; i++) { ids.push(s[i].pattern); } this.adapter.log.debug(`Subscribes: ${ids.join(', ')}`); } else { this.adapter.log.debug('Subscribes: no subscribes'); } } isLogEnabled() { return this.#logEnabled; } subscribe(socket, type, pattern, patternFile) { if (!pattern) { this.adapter.log.warn('Empty pattern on subscribe!'); return; } this.subscribes[type] ||= {}; let p; let key; pattern = pattern.toString(); if (patternFile && type === 'fileChange') { patternFile = patternFile.toString(); key = `${pattern}####${patternFile}`; } else { key = pattern; } try { p = pattern2RegEx(key); } catch (e) { this.adapter.log.error(`Invalid pattern on subscribe: ${e.message}`); return; } if (p === null) { this.adapter.log.warn('Empty pattern on subscribe!'); return; } let s; if (socket) { socket.subscribe ||= {}; socket.subscribe[type] ||= []; s = socket.subscribe[type]; if (s.find(item => item.pattern === key)) { return; } s.push({ pattern: key, regex: new RegExp(p) }); } const options = socket?._acl?.user ? { user: socket._acl.user } : undefined; if (this.subscribes[type][key] === undefined) { this.subscribes[type][key] = 1; if (type === 'stateChange') { this.adapter .subscribeForeignStatesAsync(pattern, options) .catch(e => this.adapter.log.error(`Cannot subscribe "${pattern}": ${e.message}`)); } else if (type === 'objectChange') { this.adapter .subscribeForeignObjectsAsync(pattern, options) .catch(e => this.adapter.log.error(`Cannot subscribe "${pattern}": ${e.message}`)); } else if (type === 'log') { if (!this.#logEnabled && this.adapter.requireLog) { this.#logEnabled = true; void this.adapter.requireLog(true, options); } } else if (type === 'fileChange' && this.adapter.subscribeForeignFiles) { void this.adapter .subscribeForeignFiles(pattern, patternFile || '*', options) .catch(e => this.adapter.log.error(`Cannot subscribe "${pattern}": ${e.message}`)); } } else { this.subscribes[type][key]++; } } unsubscribe(socket, type, pattern, patternFile) { if (!pattern) { this.adapter.log.warn('Empty pattern on subscribe!'); return; } if (!this.subscribes[type]) { return; } let key; pattern = pattern.toString(); if (patternFile && type === 'fileChange') { patternFile = patternFile.toString(); key = `${pattern}####${patternFile}`; } else { key = pattern; } const options = socket?._acl?.user ? { user: socket._acl.user } : undefined; if (socket && typeof socket === 'object') { if (!socket.subscribe?.[type]) { return; } for (let i = socket.subscribe[type].length - 1; i >= 0; i--) { if (socket.subscribe[type][i].pattern === key) { // Remove a pattern from a global list if (this.subscribes[type][key] !== undefined) { this.subscribes[type][key]--; if (this.subscribes[type][key] <= 0) { if (type === 'stateChange') { this.adapter .unsubscribeForeignStatesAsync(pattern, options) .catch(e => this.adapter.log.error(`Cannot unsubscribe "${pattern}": ${e.message}`)); } else if (type === 'objectChange') { this.adapter .unsubscribeForeignObjectsAsync(pattern, options) .catch(e => this.adapter.log.error(`Cannot unsubscribe "${pattern}": ${e.message}`)); } else if (type === 'log') { if (this.#logEnabled && this.adapter.requireLog) { this.#logEnabled = false; void this.adapter.requireLog(false, options); } } else if (type === 'fileChange' && this.adapter.unsubscribeForeignFiles) { void this.adapter .unsubscribeForeignFiles(pattern, patternFile || '*', options) .catch(e => this.adapter.log.error(`Cannot unsubscribe "${pattern}": ${e.message}`)); } delete this.subscribes[type][pattern]; } } socket.subscribe[type].splice(i, 1); return; } } } else if (key) { // Remove a pattern from a global list if (this.subscribes[type][key] !== undefined) { this.subscribes[type][key]--; if (this.subscribes[type][key] <= 0) { if (type === 'stateChange') { this.adapter .unsubscribeForeignStatesAsync(pattern, options) .catch(e => this.adapter.log.error(`Cannot unsubscribe "${pattern}": ${e.message}`)); } else if (type === 'objectChange') { this.adapter .unsubscribeForeignObjectsAsync(pattern, options) .catch(e => this.adapter.log.error(`Cannot unsubscribe "${pattern}": ${e.message}`)); } else if (type === 'log') { if (this.adapter.requireLog && this.#logEnabled) { this.#logEnabled = false; void this.adapter.requireLog(false, options); } } else if (type === 'fileChange' && this.adapter.unsubscribeForeignFiles) { void this.adapter .unsubscribeForeignFiles(pattern, patternFile || '*', options) .catch(e => this.adapter.log.error(`Cannot unsubscribe "${pattern}": ${e.message}`)); } delete this.subscribes[type][key]; } } } else { for (const pattern of Object.keys(this.subscribes[type])) { if (type === 'stateChange') { this.adapter .unsubscribeForeignStatesAsync(pattern, options) .catch(e => this.adapter.log.error(`Cannot unsubscribe "${pattern}": ${e.message}`)); } else if (type === 'objectChange') { this.adapter .unsubscribeForeignObjectsAsync(pattern, options) .catch(e => this.adapter.log.error(`Cannot unsubscribe "${pattern}": ${e.message}`)); } else if (type === 'log') { // console.log((socket._name || socket.id) + ' requireLog false'); if (this.adapter.requireLog && this.#logEnabled) { this.#logEnabled = false; void this.adapter.requireLog(false, options); } } else if (type === 'fileChange' && this.adapter.unsubscribeForeignFiles) { const [id, fileName] = pattern.split('####'); void this.adapter .unsubscribeForeignFiles(id, fileName, options) .catch(e => this.adapter.log.error(`Cannot unsubscribe "${pattern}": ${e.message}`)); } } this.subscribes[type] = {}; } } subscribeSocket(socket, type) { if (!socket?.subscribe) { return; } if (!type) { // all Object.keys(socket.subscribe).forEach(type => this.subscribeSocket(socket, type)); return; } if (!socket.subscribe[type]) { return; } const options = socket?._acl?.user ? { user: socket._acl.user } : undefined; for (let i = 0; i < socket.subscribe[type].length; i++) { const pattern = socket.subscribe[type][i].pattern; if (this.subscribes[type][pattern] === undefined) { this.subscribes[type][pattern] = 1; if (type === 'stateChange') { this.adapter .subscribeForeignStatesAsync(pattern, options) .catch(e => this.adapter.log.error(`Cannot subscribe "${pattern}": ${e.message}`)); } else if (type === 'objectChange') { this.adapter .subscribeForeignObjectsAsync(pattern, options) .catch(e => this.adapter.log.error(`Cannot subscribe "${pattern}": ${e.message}`)); } else if (type === 'log') { if (this.adapter.requireLog && !this.#logEnabled) { this.#logEnabled = true; void this.adapter.requireLog(true, options); } } else if (type === 'fileChange' && this.adapter.subscribeForeignFiles) { const [id, fileName] = pattern.split('####'); void this.adapter .subscribeForeignFiles(id, fileName, options) .catch(e => this.adapter.log.error(`Cannot subscribe "${pattern}": ${e.message}`)); } } else { this.subscribes[type][pattern]++; } } } unsubscribeSocket(socket, type) { if (!socket?.subscribe) { return; } // inform all instances about disconnected socket this.#informAboutDisconnect(socket.id); if (!type) { // all Object.keys(socket.subscribe).forEach(type => this.unsubscribeSocket(socket, type)); return; } if (!socket.subscribe[type]) { return; } const options = socket?._acl?.user ? { user: socket._acl.user } : undefined; for (let i = 0; i < socket.subscribe[type].length; i++) { const pattern = socket.subscribe[type][i].pattern; if (this.subscribes[type][pattern] !== undefined) { this.subscribes[type][pattern]--; if (this.subscribes[type][pattern] <= 0) { if (type === 'stateChange') { this.adapter .unsubscribeForeignStatesAsync(pattern, options) .catch(e => this.adapter.log.error(`Cannot unsubscribe "${pattern}": ${e.message}`)); } else if (type === 'objectChange') { this.adapter .unsubscribeForeignObjectsAsync(pattern, options) .catch(e => this.adapter.log.error(`Cannot unsubscribe "${pattern}": ${e.message}`)); } else if (type === 'log') { if (this.adapter.requireLog && !this.#logEnabled) { this.#logEnabled = true; void this.adapter.requireLog(true, options); } } else if (type === 'fileChange' && this.adapter.unsubscribeForeignFiles) { const [id, fileName] = pattern.split('####'); void this.adapter .unsubscribeForeignFiles(id, fileName, options) .catch(e => this.adapter.log.error(`Cannot unsubscribe "${pattern}": ${e.message}`)); } delete this.subscribes[type][pattern]; } } } } #subscribeStates(socket, pattern, callback) { if (this._checkPermissions(socket, 'subscribe', callback, pattern)) { if (Array.isArray(pattern)) { for (let p = 0; p < pattern.length; p++) { this.subscribe(socket, 'stateChange', pattern[p]); } } else { this.subscribe(socket, 'stateChange', pattern); } if (this.adapter.log.level === 'debug') { this._showSubscribes(socket, 'stateChange'); } if (typeof callback === 'function') { setImmediate(callback, null); } } } #unsubscribeStates(socket, pattern, callback) { if (this._checkPermissions(socket, 'unsubscribe', callback, pattern)) { if (Array.isArray(pattern)) { for (let p = 0; p < pattern.length; p++) { this.unsubscribe(socket, 'stateChange', pattern[p]); } } else { this.unsubscribe(socket, 'stateChange', pattern); } if (this.adapter.log.level === 'debug') { this._showSubscribes(socket, 'stateChange'); } if (typeof callback === 'function') { setImmediate(callback, null); } } } #subscribeFiles(socket, id, pattern, callback) { if (this._checkPermissions(socket, 'subscribeFiles', callback, pattern)) { if (Array.isArray(pattern)) { for (let p = 0; p < pattern.length; p++) { this.subscribe(socket, 'fileChange', id, pattern[p]); } } else { this.subscribe(socket, 'fileChange', id, pattern); } if (this.adapter.log.level === 'debug') { this._showSubscribes(socket, 'fileChange'); } if (typeof callback === 'function') { setImmediate(callback, null); } } } _unsubscribeFiles(socket, id, pattern, callback) { if (this._checkPermissions(socket, 'unsubscribeFiles', callback, pattern)) { if (Array.isArray(pattern)) { for (let p = 0; p < pattern.length; p++) { this.unsubscribe(socket, 'fileChange', id, pattern[p]); } } else { this.unsubscribe(socket, 'fileChange', id, pattern); } if (this.adapter.log.level === 'debug') { this._showSubscribes(socket, 'fileChange'); } if (typeof callback === 'function') { setImmediate(callback, null); } } } addCommandHandler(command, handler) { if (handler) { this.commands[command] = handler; } else if (command in this.commands) { delete this.commands[command]; } } getCommandHandler(command) { return this.commands[command]; } /** * Converts old structures of config definitions into new one - `adminUI` * * @param obj Instance or adapter object to be converted */ fixAdminUI(obj) { if (obj?.common && !obj.common.adminUI) { obj.common.adminUI = { config: 'none' }; if (obj.common.noConfig) { obj.common.adminUI.config = 'none'; // @ts-expect-error this attribute is deprecated, but still used } else if (obj.common.jsonConfig) { obj.common.adminUI.config = 'json'; } else if (obj.common.materialize) { obj.common.adminUI.config = 'materialize'; } else { obj.common.adminUI.config = 'html'; } // @ts-expect-error this attribute is deprecated, but still used if (obj.common.jsonCustom) { obj.common.adminUI.custom = 'json'; } else if (obj.common.supportCustoms) { obj.common.adminUI.custom = 'json'; } if (obj.common.materializeTab && obj.common.adminTab) { obj.common.adminUI.tab = 'materialize'; } else if (obj.common.adminTab) { obj.common.adminUI.tab = 'html'; } if (obj.common.adminUI) { this.adapter.log.debug(`Please add to "${obj._id.replace(/\.\d+$/, '')}" common.adminUI=${JSON.stringify(obj.common.adminUI)}`); } } } #httpGet(url, callback) { this.adapter.log.debug(`httpGet: ${url}`); if (axiosGet) { try { axiosGet(url, { responseType: 'arraybuffer', timeout: 15000, validateStatus: (status) => status < 400, }) .then((result) => callback(null, { status: result.status, statusText: result.statusText }, result.data)) .catch((error) => callback(error)); } catch (error) { callback(error); } } else { callback(new Error('axios is not initialized')); } } // Init common commands that not belong to stats, objects or files _initCommandsCommon() { /** * #DOCUMENTATION commands * Wait till the user is authenticated. * As the user authenticates himself, the callback will be called * * @param socket Socket instance * @param callback Callback `(isUserAuthenticated: boolean, isAuthenticationUsed: boolean) => void` */ this.commands.authenticate = (socket, callback) => { if (socket._acl?.user !== null) { this.adapter.log.debug(`${new Date().toISOString()} Request authenticate [${socket._acl?.user}]`); if (typeof callback === 'function') { callback(true, socket._secure); } } else { socket._authPending = callback; } }; /** * #DOCUMENTATION commands * After the access token is updated, this command must be called to update the session (Only for OAuth2) * * @param socket Socket instance * @param accessToken New access token * @param callback Callback `(error: string | undefined | null, success?: boolean) => void` */ this.commands.updateTokenExpiration = (socket, accessToken, callback) => { // Check if the user is authenticated if (accessToken) { void this.adapter.getSession(`a:${accessToken}`, (token) => { if (!token?.user) { this.adapter.log.silly('No session found'); callback('No access token found', false); } else { // Replace access token in cookie if (socket.conn.request.headers?.cookie?.includes('access_token=')) { socket.conn.request.headers.cookie = socket.conn.request.headers.cookie.replace(/access_token=[^;]+/, `access_token=${accessToken}`); } if (socket.conn.request.headers?.authorization?.startsWith('Bearer ')) { socket.conn.request.headers.authorization = `Bearer ${accessToken}`; } if (socket.conn.request.query?.token) { socket.conn.request.query.token = accessToken; } socket._sessionExpiresAt = token.aExp; callback(null, true); } }); } else { callback('No access token found', false); } }; /** * #DOCUMENTATION commands * Write error into ioBroker log * * @param _socket Socket instance (not used) * @param error Error object or error text */ this.commands.error = (_socket, error) => { this.adapter.log.error(`Socket error: ${error.toString()}`); }; /** * #DOCUMENTATION commands * Write log entry into ioBroker log * * @param _socket Socket instance (not used) * @param text log text * @param level one of `['silly', 'debug', 'info', 'warn', 'error']`. Default is 'debug'. */ this.commands.log = (_socket, text, level) => { if (level === 'error') { this.adapter.log.error(text); } else if (level === 'warn') { this.adapter.log.warn(text); } else if (level === 'info') { this.adapter.log.info(text); } else { this.adapter.log.debug(text); } }; /** * #DOCUMENTATION commands * Check if the same feature is supported by the current js-controller * * @param _socket Socket instance (not used) * @param feature feature name like `CONTROLLER_LICENSE_MANAGER` * @param callback callback `(error: string | Error | null | undefined, isSupported: boolean) => void` */ this.commands.checkFeatureSupported = (_socket, feature, callback) => { if (feature === 'INSTANCE_MESSAGES') { SocketCommands._fixCallback(callback, null, true); } else if (feature === 'PARTIAL_OBJECT_TREE') { SocketCommands._fixCallback(callback, null, true); } else { SocketCommands._fixCallback(callback, null, this.adapter.supportsFeature(feature)); } }; /** * #DOCUMENTATION commands * Get history data from specific instance * * @param socket Socket instance * @param id object ID * @param options History options * @param callback callback `(error: string | Error | null | undefined, result: ioBroker.GetHistoryResult) => void` */ this.commands.getHistory = (socket, id, options, callback) => { if (this._checkPermissions(socket, 'getStateHistory', callback, id)) { if (typeof options === 'string') { options = { instance: options, }; } options ||= {}; // @ts-expect-error fixed in js-controller options.user = socket._acl?.user; options.aggregate ||= 'none'; try { this.adapter.getHistory(id, options, (error, ...args) => SocketCommands._fixCallback(callback, error, ...args)); } catch (error) { this.adapter.log.error(`[getHistory] ERROR: ${error.toString()}`); SocketCommands._fixCallback(callback, error); } } }; /** * #DOCUMENTATION commands * Read content of HTTP(s) page server-side (without CORS and stuff) * * @param socket Socket instance * @param url Page URL * @param callback callback `(error: Error | null, result?: { status: number; statusText: string }, data?: string) => void` */ this.commands.httpGet = (socket, url, callback) => { if (this._checkPermissions(socket, 'httpGet', callback, url)) { if (axiosGet) { this.#httpGet(url, callback); } else { void import('axios').then(({ default: axios }) => { axiosGet ||= axios.get; this.#httpGet(url, callback); }); } } }; /** * #DOCUMENTATION commands * Send the message to specific instance * * @param socket Socket instance * @param adapterInstance instance name, e.g. `history.0` * @param command command name * @param message the message is instance-dependent * @param callback callback `(result: any) => void` */ this.commands.sendTo = (socket, adapterInstance, command, message, callback) => { if (this._checkPermissions(socket, 'sendTo', callback, command)) { try { this.adapter.sendTo(adapterInstance, command, message, res => typeof callback === 'function' && setImmediate(() => callback(res))); } catch (error) { if (typeof callback === 'function') { setImmediate(() => callback({ error })); } } } }; // following commands are protected and require the extra permissions const protectedCommands = [ 'cmdExec', 'getLocationOnDisk', 'getDiagData', 'getDevList', 'delLogs', 'writeDirAsZip', 'writeObjectsAsZip', 'readObjectsAsZip', 'checkLogging', 'updateMultihost', 'rebuildAdapter', ]; /** * #DOCUMENTATION commands * Send a message to the specific host. * Host can answer to the following commands: `cmdExec, getRepository, getInstalled, getInstalledAdapter, getVersion, getDiagData, getLocationOnDisk, getDevList, getLogs, getHostInfo, delLogs, readDirAsZip, writeDirAsZip, readObjectsAsZip, writeObjectsAsZip, checkLogging, updateMultihost`. * * @param socket Socket instance * @param host Host name. With or without 'system.host.' prefix * @param command Host command * @param message the message is command-specific * @param callback callback `(result: { error?: string; result?: any }) => void` */ this.commands.sendToHost = (socket, host, command, message, callback) => { if (this._checkPermissions(socket, protectedCommands.includes(command) ? 'cmdExec' : 'sendToHost', (error) => callback({ error: error || SocketCommands.ERROR_PERMISSION }), command)) { // Try to decode this file locally as redis has a limitation for files bigger than 20MB if (command === 'writeDirAsZip' && message && message.data.length > 1024 * 1024) { let buffer; try { buffer = Buffer.from(message.data, 'base64'); } catch (error) { this.adapter.log.error(`Cannot convert data: ${error.toString()}`); callback?.({ error: `Cannot convert data: ${error.toString()}` }); return; } zipFiles ||= adapter_core_1.commonTools.zipFiles; zipFiles .writeDirAsZip(this.adapter, // normally we have to pass here the internal "objects" object, but as // only writeFile is used, and it has the same name, we can pass here the // adapter, which has the function with the same name and arguments message.id, message.name, buffer, message.options, (error) => callback({ error: error?.toString() })) .then(() => callback({})) .catch((error) => { this.adapter.log.error(`Cannot write zip file as folder: ${error.toString()}`); if (callback) { callback({ error: error?.toString() }); } }); } else if (this._sendToHost) { this._sendToHost(host, command, message, callback); } else { try { this.adapter.sendToHost(host, command, message, callback); } catch (error) { if (callback) { callback({ error }); } } } } }; /** * #DOCUMENTATION commands * Ask server is authentication enabled, and if the user authenticated * * @param socket Socket instance * @param callback callback `(isUserAuthenticated: boolean | Error | string, isAuthenticationUsed: boolean) => void` */ this.commands.authEnabled = (socket, callback) => { if (this._checkPermissions(socket, 'authEnabled', callback)) { if (typeof callback === 'function') { // @ts-expect-error auth could exist in adapter settings callback(this.adapter.config.auth, (socket._acl?.user || '').replace(/^system\.user\./, '')); } else { this.adapter.log.warn('[authEnabled] Invalid callback'); } } }; /** * #DOCUMENTATION commands * Logout user * * @param socket Socket instance * @param callback callback `(error?: Error) => void` */ this.commands.logout = (socket, callback) => { // try to extract access token let accessToken; if (socket.conn.request.headers?.authorization?.startsWith('Bearer ')) { accessToken = socket.conn.request.headers.authorization.split(' ')[1]; } if (!accessToken) { // socket.io has "_query" and not "query" in the request accessToken = socket.conn.request.query?.token || socket.conn.request._query.token; } if (!accessToken) { const part = socket.conn.request.headers?.cookie ?.split(';') .find(part => part.trim().startsWith('access_token=')); if (part) { accessToken = part.trim().split('=')[1]; } } if (accessToken) { void this.adapter.getSession(`a:${accessToken}`, (token) => { if (token?.aToken) { void this.adapter.destroySession(`a:${token.aToken}`, () => { void this.adapter.destroySession(`r:${token.rToken}`, () => { if (socket.id) { void this.adapter.destroySession(socket.id, callback); } else if (callback) { callback(); } }); }); } else { if (socket.id) { void this.adapter.destroySession(socket.id, callback); } else if (callback) { callback(); } } }); } else if (socket.id) { void this.adapter.destroySession(socket.id, callback); } else if (callback) { callback(new Error('No session')); } }; /** * #DOCUMENTATION commands * List commands and permissions * * @param _socket Socket instance (not used) * @param callback callback `(permissions: Record<string, { type: 'object' | 'state' | 'users' | 'other' | 'file' | ''; operation: SocketOperation }>) => void` */ this.commands.listPermissions = (_socket, callback) => { if (typeof callback === 'function') { callback(SocketCommands.COMMANDS_PERMISSIONS); } else { this.adapter.log.warn('[listPermissions] Invalid callback'); } }; /** * #DOCUMENTATION commands * Get user permissions * * @param socket Socket instance * @param callback callback `(error: string | null | undefined, userPermissions?: SocketACL | null) => void` */ this.commands.getUserPermissions = (socket, callback) => { if (this._checkPermissions(socket, 'getUserPermissions', callback)) { if (typeof callback === 'function') { callback(null, socket._acl); } else { this.adapter.log.warn('[getUserPermissions] Invalid callback'); } } }; /** * #DOCUMENTATION commands * Get the adapter version. Not the socket-classes version! * * @param socket Socket instance * @param callback callback `(error: string | Error | null | undefined, version: string | undefined, adapterName: string) => void` */ this.commands.getVersion = (socket, callback) => { if (this._checkPermissions(socket, 'getVersion', callback)) { if (typeof callback === 'function') { callback(null, this.adapter.version, this.adapter.name); } else { this.adapter.log.warn('[getVersion] Invalid callback'); } } }; /** * #DOCUMENTATION commands * Get adapter name: "iobroker.ws", "iobroker.socketio", "iobroker.web", "iobroker.admin" * * @param socket Socket instance * @param callback callback `(error: string | Error | null | undefined, version: string | undefined, adapterName: string) => void` */ this.commands.getAdapterName = (socket, callback) => { if (this._checkPermissions(socket, 'getAdapterName', callback)) { if (typeof callback === 'function') { callback(null, this.adapter.name || 'unknown'); } else { this.adapter.log.warn('[getAdapterName] Invalid callback'); } } }; } /** Init commands for files */ _initCommandsFiles() { /** * #DOCUMENTATION files * Read a file from ioBroker DB * * @param socket Socket instance * @param adapter instance name, e.g. `vis.0` * @param fileName file name, e.g. `main/vis-views.json` * @param callback Callback `(error: null | undefined | Error | string, data: Buffer | string, mimeType: string) => void` */ this.commands.readFile = (socket, adapter, fileName, callback) => { if (this._checkPermissions(socket, 'readFile', callback, fileName)) { try { this.adapter.readFile(adapter, fileName, { user: socket._acl?.user }, (error, ...args) => SocketCommands._fixCallback(callback, error, ...args)); } catch (error) { this.adapter.log.error(`[readFile] ERROR: ${error.toString()}`); SocketCommands._fixCallback(callback, error); } } }; /** * #DOCUMENTATION files * Read a file from ioBroker DB as base64 string * * @param socket Socket instance * @param adapter instance name, e.g. `vis.0` * @param fileName file name, e.g. `main/vis-views.json` * @param callback Callback `(error: null | undefined | Error | string, base64: string, mimeType: string) => void` */ this.commands.readFile64 = (socket, adapter, fileName, callback) => { if (this._checkPermissions(socket, 'readFile64', callback, fileName)) { try { this.adapter.readFile(adapter, fileName, { user: socket._acl?.user }, (error, buffer, type) => { let data64; if (buffer) { try { if (type === 'application/json' || type === 'application/json5' || fileName.toLowerCase().endsWith('.json5')) { data64 = Buffer.from(encodeURIComponent(buffer)).toString('base64'); } else {