@iobroker/socket-classes
Version:
ioBroker server-side web sockets
1,160 lines (1,159 loc) • 98.9 kB
JavaScript
"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 {