diff --git a/Gruntfile.js b/Gruntfile.js index f1f0aaefd..979b38051 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -165,6 +165,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/common/autoComplete.js", "packages/node_modules/@node-red/editor-client/src/js/ui/actions.js", "packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js", "packages/node_modules/@node-red/editor-client/src/js/ui/diff.js", "packages/node_modules/@node-red/editor-client/src/js/ui/keyboard.js", "packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js", diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js b/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js new file mode 100644 index 000000000..98710a069 --- /dev/null +++ b/packages/node_modules/@node-red/editor-api/lib/admin/diagnostics.js @@ -0,0 +1,23 @@ +let runtimeAPI; +let settings; +const apiUtil = require("../util"); +module.exports = { + init: function(_settings, _runtimeAPI) { + settings = _settings; + runtimeAPI = _runtimeAPI; + }, + getReport: function(req, res) { + const diagnosticsOpts = settings.diagnostics || {}; + const opts = { + user: req.user, + scope: diagnosticsOpts.level || "basic" + } + if(diagnosticsOpts.enabled === false || diagnosticsOpts.enabled === "false") { + apiUtil.rejectHandler(req, res, {message: "diagnostics are disabled", status: 403, code: "diagnostics.disabled" }) + } else { + runtimeAPI.diagnostics.get(opts) + .then(function(result) { res.json(result); }) + .catch(err => apiUtil.rejectHandler(req, res, err)) + } + } +} diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index be9bb5317..87fd0dec0 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -23,6 +23,7 @@ var context = require("./context"); var auth = require("../auth"); var info = require("./settings"); var plugins = require("./plugins"); +var diagnostics = require("./diagnostics"); var apiUtil = require("../util"); @@ -34,6 +35,7 @@ module.exports = { context.init(runtimeAPI); info.init(settings,runtimeAPI); plugins.init(runtimeAPI); + diagnostics.init(settings, runtimeAPI); var needsPermission = auth.needsPermission; @@ -95,6 +97,8 @@ module.exports = { adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler); adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler); + adminApp.get("/diagnostics", needsPermission("diagnostics.read"), diagnostics.getReport, apiUtil.errorHandler); + return adminApp; } } diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index cf4761a03..8551d39a6 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -940,6 +940,8 @@ "format": "format JSON", "rawMode": "Edit JSON", "uiMode": "Visual editor", + "rawMode-readonly": "JSON", + "uiMode-readonly": "Visual", "insertAbove": "Insert above", "insertBelow": "Insert below", "addItem": "Add item", @@ -1154,6 +1156,9 @@ "start": "Start", "next": "Next" }, + "diagnostics": { + "title": "System Info" + }, "languages" : { "de": "German", "en-US": "English", diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json index d6c5db150..a6326b635 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -940,6 +940,8 @@ "format": "JSONフォーマット", "rawMode": "JSONを編集", "uiMode": "ビジュアルエディタ", + "rawMode-readonly": "JSON", + "uiMode-readonly": "ビジュアル", "insertAbove": "上に挿入", "insertBelow": "下に挿入", "addItem": "要素を追加", @@ -1298,22 +1300,23 @@ "zoom-in": "ズームイン", "zoom-out": "ズームアウト", "zoom-reset": "ズームリセット", - "toggle-navigator": "ナビゲータ表示切替" + "toggle-navigator": "ナビゲータ表示切替", + "show-system-info": "システムインフォメーション" }, "validator": { "errors": { - "invalid-json": "JSONデータが不正: __error__", - "invalid-json-prop": "__prop__: JSONデータが不正: __error__", - "invalid-prop": "プロパティ式が不正", - "invalid-prop-prop": "__prop__: プロパティ式が不正", - "invalid-num": "数値が不正", - "invalid-num-prop": "__prop__: 数値が不正", - "invalid-regexp": "入力パターンが不正", - "invalid-regex-prop": "__prop__: 入力パターンが不正", - "missing-required-prop": "__prop__: プロパティが未設定", - "invalid-config": "__prop__: 設定ノードが不正", - "missing-config": "__prop__: 設定ノードが存在しません", - "validation-error": "__prop__: チェックエラー: __node__, __id__: __error__" - } + "invalid-json": "JSONデータが不正: __error__", + "invalid-json-prop": "__prop__: JSONデータが不正: __error__", + "invalid-prop": "プロパティ式が不正", + "invalid-prop-prop": "__prop__: プロパティ式が不正", + "invalid-num": "数値が不正", + "invalid-num-prop": "__prop__: 数値が不正", + "invalid-regexp": "入力パターンが不正", + "invalid-regex-prop": "__prop__: 入力パターンが不正", + "missing-required-prop": "__prop__: プロパティが未設定", + "invalid-config": "__prop__: 設定ノードが不正", + "missing-config": "__prop__: 設定ノードが存在しません", + "validation-error": "__prop__: チェックエラー: __node__, __id__: __error__" + } } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index c24b0fa57..47f206a46 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -730,6 +730,7 @@ var RED = (function() { RED.search.init(); RED.actionList.init(); RED.editor.init(); + RED.diagnostics.init(); RED.diff.init(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js b/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js new file mode 100644 index 000000000..4e4a2d657 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/diagnostics.js @@ -0,0 +1,61 @@ + +RED.diagnostics = (function () { + + function init() { + if (RED.settings.get('diagnostics.ui', true) === false) { + return; + } + RED.actions.add("core:show-system-info", function () { show(); }); + } + + function show() { + $.ajax({ + headers: { + "Accept": "application/json" + }, + cache: false, + url: 'diagnostics', + success: function (data) { + var json = JSON.stringify(data || {}, "", 4); + if (json === "{}") { + json = "{\n\n}"; + } + RED.editor.editJSON({ + title: RED._('diagnostics.title'), + value: json, + requireValid: true, + readOnly: true, + toolbarButtons: [ + { + text: RED._('clipboard.export.copy'), + icon: 'fa fa-copy', + click: function () { + RED.clipboard.copyText(json, $(this), RED._('clipboard.copyMessageValue')) + } + }, + { + text: RED._('clipboard.download'), + icon: 'fa fa-download', + click: function () { + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(json)); + element.setAttribute('download', "system-info.json"); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + } + }, + ] + }); + }, + error: function (jqXHR, textStatus, errorThrown) { + console.log("Unexpected error loading system info:", jqXHR.status, textStatus, errorThrown); + } + }); + } + + return { + init: init, + }; +})(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js index 84d2bce93..afb0698a1 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js @@ -21,7 +21,9 @@ ''+ '
'+ '
'+ - ''+ + ''+ + ''+ + ''+ '
'+ '
'+ '
'+ @@ -34,7 +36,7 @@ var activeTab; - function insertNewItem(parent,index,copyIndex) { + function insertNewItem(parent,index,copyIndex,readOnly) { var newValue = ""; if (parent.children.length > 0) { @@ -60,26 +62,26 @@ newKey = keyRoot+"-"+(keySuffix++); } } - var newItem = handleItem(newKey,newValue,parent.depth+1,parent); + var newItem = handleItem(newKey,newValue,parent.depth+1,parent,readOnly); parent.treeList.insertChildAt(newItem, index, true); parent.treeList.expand(); } - function showObjectMenu(button,item) { + function showObjectMenu(button,item,readOnly) { var elementPos = button.offset(); var options = []; if (item.parent) { options.push({id:"red-ui-editor-type-json-menu-insert-above", icon:"fa fa-toggle-up", label:RED._('jsonEditor.insertAbove'),onselect:function(){ var index = item.parent.children.indexOf(item); - insertNewItem(item.parent,index,index); + insertNewItem(item.parent,index,index,readOnly); }}); options.push({id:"red-ui-editor-type-json-menu-insert-below", icon:"fa fa-toggle-down", label:RED._('jsonEditor.insertBelow'),onselect:function(){ var index = item.parent.children.indexOf(item)+1; - insertNewItem(item.parent,index,index-1); + insertNewItem(item.parent,index,index-1,readOnly); }}); } if (item.type === 'array' || item.type === 'object') { options.push({id:"red-ui-editor-type-json-menu-add-child", icon:"fa fa-plus", label:RED._('jsonEditor.addItem'),onselect:function(){ - insertNewItem(item,item.children.length,item.children.length-1); + insertNewItem(item,item.children.length,item.children.length-1,readOnly); }}); } if (item.parent) { @@ -121,7 +123,7 @@ newKey = keyRoot+"-"+(keySuffix++); } } - var newItem = handleItem(newKey,convertToObject(item),item.parent.depth+1,item.parent); + var newItem = handleItem(newKey,convertToObject(item),item.parent.depth+1,item.parent,readOnly); var index = item.parent.children.indexOf(item)+1; item.parent.treeList.insertChildAt(newItem, index, true); @@ -171,24 +173,24 @@ menuOptionMenu.show(); } - function parseObject(obj,depth,parent) { + function parseObject(obj,depth,parent,readOnly) { var result = []; for (var prop in obj) { if (obj.hasOwnProperty(prop)) { - result.push(handleItem(prop,obj[prop],depth,parent)); + result.push(handleItem(prop,obj[prop],depth,parent,readOnly)); } } return result; } - function parseArray(obj,depth,parent) { + function parseArray(obj,depth,parent,readOnly) { var result = []; var l = obj.length; for (var i=0;i'); if (key != null) { @@ -204,11 +206,14 @@ if (parent && parent.type === "array") { keyLabel.addClass("red-ui-editor-type-json-editor-label-array-key") } - + if(readOnly) { + keyLabel.addClass("readonly") + } keyLabel.on("click", function(evt) { if (item.parent.type === 'array') { return; } + if (readOnly) { return; } evt.preventDefault(); evt.stopPropagation(); var w = Math.max(150,keyLabel.width()); @@ -253,10 +258,10 @@ item.expanded = depth < 2; item.type = "array"; item.deferBuild = depth >= 2; - item.children = parseArray(val,depth+1,item); + item.children = parseArray(val,depth+1,item,readOnly); } else if (val !== null && item.type === "object") { item.expanded = depth < 2; - item.children = parseObject(val,depth+1,item); + item.children = parseObject(val,depth+1,item,readOnly); item.deferBuild = depth >= 2; } else { item.value = val; @@ -287,7 +292,11 @@ // var orphanedChildren; var valueLabel = $('').addClass(valClass).text(valValue).appendTo(container); + if (readOnly) { + valueLabel.addClass("readonly") + } valueLabel.on("click", function(evt) { + if (readOnly) { return; } evt.preventDefault(); evt.stopPropagation(); if (valType === 'str') { @@ -395,17 +404,19 @@ valueLabel.hide(); }) item.gutter = $(''); - - if (parent) {//red-ui-editor-type-json-editor-item-handle - $('').appendTo(item.gutter); - } else { - $('').appendTo(item.gutter); + if(!readOnly) { + if (parent) { + $('').appendTo(item.gutter); + } else { + $('').appendTo(item.gutter); + } + $('').appendTo(item.gutter).on("click", function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + showObjectMenu($(this), item, readOnly); + }); } - $('').appendTo(item.gutter).on("click", function(evt) { - evt.preventDefault(); - evt.stopPropagation(); - showObjectMenu($(this), item); - }); + item.element = container; return item; } @@ -501,7 +512,25 @@ open: function(tray) { var trayBody = tray.find('.red-ui-tray-body'); var dialogForm = RED.editor.buildEditForm(tray.find('.red-ui-tray-body'),'dialog-form',type,'editor'); - + var toolbarButtons = options.toolbarButtons || []; + if (toolbarButtons.length) { + toolbarButtons.forEach(function (button) { + var element = $('') + .insertBefore("#node-input-json-reformat") + .on("click", function (evt) { + evt.preventDefault(); + if (button.click !== undefined) { + button.click.call(element, evt); + } + }); + if (button.id) { element.attr("id", button.id); } + if (button.title) { element.attr("title", button.title); } + if (button.icon) { element.append($("").attr("class", button.icon)); } + if (button.label || button.text) { + element.append($("").text(" " + (button.label || button.text))); + } + }); + } var container = $("#red-ui-editor-type-json-tab-ui-container").css({"height":"100%"}); var filterDepth = Infinity; var list = $('
').appendTo(container).treeList({ @@ -531,11 +560,13 @@ }) }); - expressionEditor = RED.editor.createEditor({ id: 'node-input-json', + value: "", + mode:"ace/mode/json", value: value||"", mode:"ace/mode/json", + readOnly: !!options.readOnly, stateId: options.stateId, focus: true }); @@ -576,7 +607,7 @@ var raw = expressionEditor.getValue().trim() ||"{}"; try { var parsed = JSON.parse(raw); - rootNode = handleItem(null,parsed,0,null); + rootNode = handleItem(null,parsed,0,null,options.readOnly); rootNode.class = "red-ui-editor-type-json-root-node" list.treeList('data',[rootNode]); } catch(err) { @@ -594,12 +625,12 @@ tabs.addTab({ id: 'json-raw', - label: RED._('jsonEditor.rawMode'), + label: options.readOnly ? RED._('jsonEditor.rawMode-readonly') : RED._('jsonEditor.rawMode'), content: $("#red-ui-editor-type-json-tab-raw") }); tabs.addTab({ id: 'json-ui', - label: RED._('jsonEditor.uiMode'), + label: options.readOnly ? RED._('jsonEditor.uiMode-readonly') : RED._('jsonEditor.uiMode'), content: $("#red-ui-editor-type-json-tab-ui") }); finishedBuild = true; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/editor.scss b/packages/node_modules/@node-red/editor-client/src/sass/editor.scss index dee89035a..75133df8a 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/editor.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/editor.scss @@ -701,6 +701,10 @@ div.red-ui-button-small.red-ui-color-picker-opacity-slider-handle { border-color: $list-item-background-hover; border-style: dashed; } + &.readonly { + cursor: pointer; + pointer-events: none; + } } .red-ui-editor-type-json-editor-item-gutter { width: 48px; @@ -720,6 +724,10 @@ div.red-ui-button-small.red-ui-color-picker-opacity-slider-handle { > span, > button { display: none; } + &.readonly { + cursor: pointer; + pointer-events: none; + } } diff --git a/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js new file mode 100644 index 000000000..0293c7f97 --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/api/diagnostics.js @@ -0,0 +1,202 @@ + +const os = require('os'); +const fs = require('fs'); + +let runtime; +let isContainerCached; +let isWSLCached; + +const isInWsl = () => { + if (isWSLCached === undefined) { + isWSLCached = getIsInWSL(); + } + return isWSLCached; + function getIsInWSL() { + if (process.platform !== 'linux') { + return false; + } + try { + if (os.release().toLowerCase().includes('microsoft')) { + if (isInContainer()) { + return false; + } + return true; + } + return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft') ? !isInContainer() : false; + } catch (_) { + return false; + } + } +}; + +const isInContainer = () => { + if (isContainerCached === undefined) { + isContainerCached = hasDockerEnv() || hasDockerCGroup(); + } + return isContainerCached; + function hasDockerEnv() { + try { + fs.statSync('/.dockerenv'); + return true; + } catch { + return false; + } + } + function hasDockerCGroup() { + try { + const s = fs.readFileSync('/proc/self/cgroup', 'utf8'); + if (s.includes('docker')) { + return "docker" + } else if (s.includes('kubepod')) { + return "kubepod" + } else if (s.includes('lxc')) { + return "lxc" + } + } catch { + return false; + } + } +} + +function buildDiagnosticReport(scope, callback) { + const modules = {}; + const nl = runtime.nodes.getNodeList(); + for (let i = 0; i < nl.length; i++) { + if (modules[nl[i].module]) { + continue; + } + modules[nl[i].module] = nl[i].version + } + + const now = new Date(); + const {locale, timeZone} = Intl.DateTimeFormat().resolvedOptions(); + const report = { + report: "diagnostics", + scope: scope, + time: { + utc: now.toUTCString(), + local: now.toLocaleString(), + }, + intl: { + locale, timeZone + }, + nodejs: { + version: process.version, + arch: process.arch, + platform: process.platform, + memoryUsage: process.memoryUsage(), + }, + os: { + containerised: isInContainer(), + wsl: isInWsl(), + totalmem: os.totalmem(), + freemem: os.freemem(), + arch: os.arch(), + loadavg: os.loadavg(), + platform: os.platform(), + release: os.release(), + type: os.type(), + uptime: os.uptime(), + version: os.version(), + }, + runtime: { + isStarted: runtime.isStarted(), + modules: modules, + version: runtime.settings.version, + settings: { + available: runtime.settings.available(), + apiMaxLength: runtime.settings.apiMaxLength || "UNSET", + //coreNodesDir: runtime.settings.coreNodesDir, + disableEditor: runtime.settings.disableEditor, + contextStorage: listContextModules(), + debugMaxLength: runtime.settings.debugMaxLength || "UNSET", + editorTheme: runtime.settings.editorTheme || "UNSET", + flowFile: runtime.settings.flowFile || "UNSET", + mqttReconnectTime: runtime.settings.mqttReconnectTime || "UNSET", + serialReconnectTime: runtime.settings.serialReconnectTime || "UNSET", + + adminAuth: runtime.settings.adminAuth ? "SET" : "UNSET", + + httpAdminRoot: runtime.settings.httpAdminRoot || "UNSET", + httpAdminCors: runtime.settings.httpAdminCors ? "SET" : "UNSET", + httpNodeAuth: runtime.settings.httpNodeAuth ? "SET" : "UNSET", + + httpNodeRoot: runtime.settings.httpNodeRoot || "UNSET", + httpNodeCors: runtime.settings.httpNodeCors ? "SET" : "UNSET", + + httpStatic: runtime.settings.httpStatic ? "SET" : "UNSET", + httpStaticRoot: runtime.settings.httpStaticRoot || "UNSET", + httpStaticCors: runtime.settings.httpStaticCors ? "SET" : "UNSET", + + uiHost: runtime.settings.uiHost ? "SET" : "UNSET", + uiPort: runtime.settings.uiPort ? "SET" : "UNSET", + userDir: runtime.settings.userDir ? "SET" : "UNSET", + } + } + } + + // if (scope == "admin") { + // const moreSettings = { + // adminAuth_type: (runtime.settings.adminAuth && runtime.settings.adminAuth.type) ? runtime.settings.adminAuth.type : "UNSET", + // httpAdminCors: runtime.settings.httpAdminCors ? runtime.settings.httpAdminCors : "UNSET", + // httpNodeCors: runtime.settings.httpNodeCors ? runtime.settings.httpNodeCors : "UNSET", + // httpStaticCors: runtime.settings.httpStaticCors ? "SET" : "UNSET", + // settingsFile: runtime.settings.settingsFile ? runtime.settings.settingsFile : "UNSET", + // uiHost: runtime.settings.uiHost ? runtime.settings.uiHost : "UNSET", + // uiPort: runtime.settings.uiPort ? runtime.settings.uiPort : "UNSET", + // userDir: runtime.settings.userDir ? runtime.settings.userDir : "UNSET", + // } + // const moreNodejs = { + // execPath: process.execPath, + // pid: process.pid, + // } + // const moreOs = { + // cpus: os.cpus(), + // homedir: os.homedir(), + // hostname: os.hostname(), + // networkInterfaces: os.networkInterfaces(), + // } + // report.runtime.settings = Object.assign({}, report.runtime.settings, moreSettings); + // report.nodejs = Object.assign({}, report.nodejs, moreNodejs); + // report.os = Object.assign({}, report.os, moreOs); + // } + + callback(report); + + /** gets a sanitised list containing only the module name */ + function listContextModules() { + const keys = Object.keys(runtime.settings.contextStorage); + const result = {}; + keys.forEach(e => { + result[e] = { + module: String(runtime.settings.contextStorage[e].module) + } + }) + return result; + } +} + + +module.exports = { + init: function (_runtime) { + runtime = _runtime; + }, + /** + * Gets the node-red diagnostics report + * @param {{scope: string}} opts - settings + * @return {Promise} the diagnostics information + * @memberof @node-red/diagnostics + */ + get: async function (opts) { + return new Promise(function (resolve, reject) { + opts = opts || {} + try { + runtime.log.audit({ event: "diagnostics.get", scope: opts.scope }, opts.req); + buildDiagnosticReport(opts.scope, (report) => resolve(report)); + } catch (error) { + error.status = 500; + reject(error); + } + }) + }, +} diff --git a/packages/node_modules/@node-red/runtime/lib/api/index.js b/packages/node_modules/@node-red/runtime/lib/api/index.js index 46d15d1e7..d2fa3c6fd 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/index.js +++ b/packages/node_modules/@node-red/runtime/lib/api/index.js @@ -29,6 +29,7 @@ var api = module.exports = { api.projects.init(runtime); api.context.init(runtime); api.plugins.init(runtime); + api.diagnostics.init(runtime); }, comms: require("./comms"), @@ -39,6 +40,7 @@ var api = module.exports = { projects: require("./projects"), context: require("./context"), plugins: require("./plugins"), + diagnostics: require("./diagnostics"), isStarted: async function(opts) { return runtime.isStarted(); diff --git a/packages/node_modules/@node-red/runtime/lib/api/settings.js b/packages/node_modules/@node-red/runtime/lib/api/settings.js index 0224c0ca6..f56b8ab61 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/settings.js +++ b/packages/node_modules/@node-red/runtime/lib/api/settings.js @@ -142,6 +142,13 @@ var api = module.exports = { } safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType(); + + safeSettings.diagnostics = { + //unless diagnostics.ui and diagnostics.enabled are explicitly false, they will default to true. + enabled: (runtime.settings.diagnostics && runtime.settings.diagnostics.enabled === false) ? false : true, + ui: (runtime.settings.diagnostics && runtime.settings.diagnostics.ui === false) ? false : true + } + runtime.settings.exportNodeSettings(safeSettings); runtime.plugins.exportPluginSettings(safeSettings); } diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index b869ef3ce..8e1d2b487 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -399,7 +399,12 @@ module.exports = { * @memberof @node-red/runtime */ version: externalAPI.version, - + + /** + * @memberof @node-red/diagnostics + */ + diagnostics:externalAPI.diagnostics, + storage: storage, events: events, hooks: hooks, diff --git a/packages/node_modules/node-red/lib/red.js b/packages/node_modules/node-red/lib/red.js index 2c6f00a6e..ddc140cc4 100644 --- a/packages/node_modules/node-red/lib/red.js +++ b/packages/node_modules/node-red/lib/red.js @@ -229,5 +229,12 @@ module.exports = { * @see @node-red/editor-api_auth * @memberof node-red */ - auth: api.auth + auth: api.auth, + + /** + * The editor authentication api. + * @see @node-red/editor-api_auth + * @memberof node-red + */ + get diagnostics() { return api.diagnostics } }; diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index 65d20e907..2e2b7035e 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -242,6 +242,7 @@ module.exports = { /******************************************************************************* * Runtime Settings * - lang + * - diagnostics * - logging * - contextStorage * - exportGlobalContextKeys @@ -254,6 +255,19 @@ module.exports = { */ // lang: "de", + /** Configure diagnostics options + * - enabled: When `enabled` is `true` (or unset), diagnostics data will + * be available at http://localhost:1880/diagnostics + * - ui: When `ui` is `true` (or unset), the action `show-system-info` will + * be available to logged in users of node-red editor + */ + diagnostics: { + /** enable or disable diagnostics endpoint. Must be set to `false` to disable */ + enabled: true, + /** enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */ + ui: true, + }, + /** Configure the logging output */ logging: { /** Only console logging is currently supported */ diff --git a/test/unit/@node-red/editor-api/lib/admin/diagnostics_spec.js b/test/unit/@node-red/editor-api/lib/admin/diagnostics_spec.js new file mode 100644 index 000000000..16d1e5b94 --- /dev/null +++ b/test/unit/@node-red/editor-api/lib/admin/diagnostics_spec.js @@ -0,0 +1,119 @@ +const should = require("should"); +const request = require('supertest'); +const express = require('express'); +const bodyParser = require("body-parser"); +const sinon = require('sinon'); + +let app; + +const NR_TEST_UTILS = require("nr-test-utils"); +const diagnostics = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/diagnostics"); + +describe("api/editor/diagnostics", function() { + before(function() { + app = express(); + app.use(bodyParser.json()); + app.get("/diagnostics",diagnostics.getReport); + }); + + it('returns the diagnostics report when explicitly enabled', function(done) { + const settings = { diagnostics: { ui: true, enabled: true } } + const runtimeAPI = { + diagnostics: { + get: async function (opts) { + return new Promise(function (resolve, reject) { + opts = opts || {} + try { + resolve({ opts: opts, a:1, b:2}); + } catch (error) { + error.status = 500; + reject(error); + } + }) + } + } + } + + diagnostics.init(settings, runtimeAPI); + + request(app) + .get("/diagnostics") + .expect(200) + .end(function(err,res) { + if (err || typeof res.error === "object") { + return done(err || res.error); + } + res.should.have.property("statusCode",200); + res.body.should.have.property("a",1); + res.body.should.have.property("b",2); + done(); + }); + }); + it('returns the diagnostics report when not explicitly enabled (implicitly enabled)', function(done) { + const settings = { diagnostics: { enabled: undefined } } + const runtimeAPI = { + diagnostics: { + get: async function (opts) { + return new Promise(function (resolve, reject) { + opts = opts || {} + try { + resolve({ opts: opts, a:3, b:4}); + } catch (error) { + error.status = 500; + reject(error); + } + }) + } + } + } + + diagnostics.init(settings, runtimeAPI); + + request(app) + .get("/diagnostics") + .expect(200) + .end(function(err,res) { + if (err || typeof res.error === "object") { + return done(err || res.error); + } + res.should.have.property("statusCode",200); + res.body.should.have.property("a",3); + res.body.should.have.property("b",4); + done(); + }); + }); + it('should error when setting is disabled', function(done) { + const settings = { diagnostics: { ui: true, enabled: false } } + const runtimeAPI = { + diagnostics: { + get: async function (opts) { + return new Promise(function (resolve, reject) { + opts = opts || {} + try { + resolve({ opts: opts}); + } catch (error) { + error.status = 500; + reject(error); + } + }) + } + } + } + + diagnostics.init(settings, runtimeAPI); + + request(app) + .get("/diagnostics") + .expect(403) + .end(function(err,res) { + if (!err && typeof res.error !== "object") { + return done(new Error("accessing diagnostics endpoint while disabled should raise error")); + } + res.should.have.property("statusCode",403); + res.body.should.have.property("message","diagnostics are disabled"); + res.body.should.have.property("code","diagnostics.disabled"); + done(); + }); + }); + +}); diff --git a/test/unit/@node-red/runtime/lib/api/diagnostics_spec.js b/test/unit/@node-red/runtime/lib/api/diagnostics_spec.js new file mode 100644 index 000000000..07e499344 --- /dev/null +++ b/test/unit/@node-red/runtime/lib/api/diagnostics_spec.js @@ -0,0 +1,126 @@ + +var should = require("should"); +var sinon = require("sinon"); +var NR_TEST_UTILS = require("nr-test-utils"); +var diagnostics = NR_TEST_UTILS.require("@node-red/runtime/lib/api/diagnostics") + +var mockLog = () => ({ + log: sinon.stub(), + debug: sinon.stub(), + trace: sinon.stub(), + warn: sinon.stub(), + info: sinon.stub(), + metric: sinon.stub(), + audit: sinon.stub(), + _: function() { return "abc"} +}) + +describe("runtime-api/diagnostics", function() { + + describe("get", function() { + before(function() { + diagnostics.init({ + isStarted: () => true, + nodes: { + getNodeList: () => [{module:"node-red", version:"9.9.9"},{module:"node-red-node-inject", version:"8.8.8"}] + }, + settings: { + version: "7.7.7", + available: () => true, + //apiMaxLength: xxx, deliberately left blank. Should arrive in report as "UNSET" + debugMaxLength: 1111, + disableEditor: false, + flowFile: "flows.json", + mqttReconnectTime: 321, + serialReconnectTime: 432, + adminAuth: {},//should be sanitised to "SET" + httpAdminRoot: "/admin/root/", + httpAdminCors: {},//should be sanitised to "SET" + httpNodeAuth: {},//should be sanitised to "SET" + httpNodeRoot: "/node/root/", + httpNodeCors: {},//should be sanitised to "SET" + httpStatic: "/var/static/",//should be sanitised to "SET" + httpStaticRoot: "/static/root/", + httpStaticCors: {},//should be sanitised to "SET" + uiHost: "something.secret.com",//should be sanitised to "SET" + uiPort: 1337,//should be sanitised to "SET" + userDir: "/var/super/secret/",//should be sanitised to "SET", + contextStorage: { + default : { module: "memory" }, + file: { module: "localfilesystem" }, + secured: { module: "secure_store", user: "fred", pass: "super-duper-secret" }, + }, + editorTheme: {} + }, + log: mockLog() + }); + }) + it("returns basic user settings", function() { + return diagnostics.get({scope:"fake_scope"}).then(result => { + should(result).be.type("object"); + + //result.xxxxx + Object.keys(result) + const reportPropCount = Object.keys(result).length; + reportPropCount.should.eql(7);//ensure no more than 7 keys are present in the report (avoid leakage of extra info) + result.should.have.property("report","diagnostics"); + result.should.have.property("scope","fake_scope"); + result.should.have.property("time").type("object"); + result.should.have.property("intl").type("object"); + result.should.have.property("nodejs").type("object"); + result.should.have.property("os").type("object"); + result.should.have.property("runtime").type("object"); + + //result.runtime.xxxxx + const runtimeCount = Object.keys(result.runtime).length; + runtimeCount.should.eql(4);//ensure no more than 4 keys are present in runtime + result.runtime.should.have.property('isStarted',true) + result.runtime.should.have.property('modules').type("object"); + result.runtime.should.have.property('settings').type("object"); + result.runtime.should.have.property('version','7.7.7'); + + //result.runtime.modules.xxxxx + const moduleCount = Object.keys(result.runtime.modules).length; + moduleCount.should.eql(2);//ensure no more than the 2 modules specified are present + result.runtime.modules.should.have.property('node-red','9.9.9'); + result.runtime.modules.should.have.property('node-red-node-inject','8.8.8'); + + //result.runtime.settings.xxxxx + const settingsCount = Object.keys(result.runtime.settings).length; + settingsCount.should.eql(21);//ensure no more than the 21 settings listed below are present in the settings object + result.runtime.settings.should.have.property('available',true); + result.runtime.settings.should.have.property('apiMaxLength', "UNSET");//deliberately disabled to ensure UNSET is returned + result.runtime.settings.should.have.property('debugMaxLength', 1111); + result.runtime.settings.should.have.property('disableEditor', false); + result.runtime.settings.should.have.property('editorTheme', {}); + result.runtime.settings.should.have.property('flowFile', "flows.json"); + result.runtime.settings.should.have.property('mqttReconnectTime', 321); + result.runtime.settings.should.have.property('serialReconnectTime', 432); + result.runtime.settings.should.have.property("adminAuth", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property("httpAdminCors", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property('httpAdminRoot', "/admin/root/"); + result.runtime.settings.should.have.property("httpNodeAuth", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property("httpNodeCors", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property('httpNodeRoot', "/node/root/"); + result.runtime.settings.should.have.property("httpStatic", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property('httpStaticRoot', "/static/root/"); + result.runtime.settings.should.have.property("httpStaticCors", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property("uiHost", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property("uiPort", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property("userDir", "SET"); //should be sanitised to "SET" + result.runtime.settings.should.have.property('contextStorage').type("object"); + + //result.runtime.settings.contextStorage.xxxxx + const contextCount = Object.keys(result.runtime.settings.contextStorage).length; + contextCount.should.eql(3);//ensure no more than the 3 settings listed below are present in the contextStorage object + result.runtime.settings.contextStorage.should.have.property('default', {module:"memory"}); + result.runtime.settings.contextStorage.should.have.property('file', {module:"localfilesystem"}); + result.runtime.settings.contextStorage.should.have.property('secured', {module:"secure_store"}); //only module should be present, other fields are dropped for security + + }) + }) + + }); + + +});