Enable/Disable Sandboxed JavaScript environment. UI: Tidy button to format java scripts.
This commit is contained in:
parent
af5791ab7a
commit
efbc65e11f
@ -20,10 +20,13 @@ import com.google.common.util.concurrent.Futures;
|
|||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import delight.nashornsandbox.NashornSandbox;
|
import delight.nashornsandbox.NashornSandbox;
|
||||||
import delight.nashornsandbox.NashornSandboxes;
|
import delight.nashornsandbox.NashornSandboxes;
|
||||||
|
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import javax.annotation.PreDestroy;
|
import javax.annotation.PreDestroy;
|
||||||
|
import javax.script.Invocable;
|
||||||
|
import javax.script.ScriptEngine;
|
||||||
import javax.script.ScriptException;
|
import javax.script.ScriptException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -35,7 +38,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class AbstractNashornJsSandboxService implements JsSandboxService {
|
public abstract class AbstractNashornJsSandboxService implements JsSandboxService {
|
||||||
|
|
||||||
private NashornSandbox sandbox = NashornSandboxes.create();
|
private NashornSandbox sandbox;
|
||||||
|
private ScriptEngine engine;
|
||||||
private ExecutorService monitorExecutorService;
|
private ExecutorService monitorExecutorService;
|
||||||
|
|
||||||
private Map<UUID, String> functionsMap = new ConcurrentHashMap<>();
|
private Map<UUID, String> functionsMap = new ConcurrentHashMap<>();
|
||||||
@ -44,11 +48,17 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
|
|||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize());
|
if (useJsSandbox()) {
|
||||||
sandbox.setExecutor(monitorExecutorService);
|
sandbox = NashornSandboxes.create();
|
||||||
sandbox.setMaxCPUTime(getMaxCpuTime());
|
monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize());
|
||||||
sandbox.allowNoBraces(false);
|
sandbox.setExecutor(monitorExecutorService);
|
||||||
sandbox.setMaxPreparedStatements(30);
|
sandbox.setMaxCPUTime(getMaxCpuTime());
|
||||||
|
sandbox.allowNoBraces(false);
|
||||||
|
sandbox.setMaxPreparedStatements(30);
|
||||||
|
} else {
|
||||||
|
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
|
||||||
|
engine = factory.getScriptEngine(new String[]{"--no-java"});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreDestroy
|
@PreDestroy
|
||||||
@ -58,6 +68,8 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract boolean useJsSandbox();
|
||||||
|
|
||||||
protected abstract int getMonitorThreadPoolSize();
|
protected abstract int getMonitorThreadPoolSize();
|
||||||
|
|
||||||
protected abstract long getMaxCpuTime();
|
protected abstract long getMaxCpuTime();
|
||||||
@ -70,7 +82,11 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
|
|||||||
String functionName = "invokeInternal_" + scriptId.toString().replace('-','_');
|
String functionName = "invokeInternal_" + scriptId.toString().replace('-','_');
|
||||||
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
|
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
|
||||||
try {
|
try {
|
||||||
sandbox.eval(jsScript);
|
if (useJsSandbox()) {
|
||||||
|
sandbox.eval(jsScript);
|
||||||
|
} else {
|
||||||
|
engine.eval(jsScript);
|
||||||
|
}
|
||||||
functionsMap.put(scriptId, functionName);
|
functionsMap.put(scriptId, functionName);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Failed to compile JS script: {}", e.getMessage(), e);
|
log.warn("Failed to compile JS script: {}", e.getMessage(), e);
|
||||||
@ -87,7 +103,13 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
|
|||||||
}
|
}
|
||||||
if (!isBlackListed(scriptId)) {
|
if (!isBlackListed(scriptId)) {
|
||||||
try {
|
try {
|
||||||
return Futures.immediateFuture(sandbox.getSandboxedInvocable().invokeFunction(functionName, args));
|
Object result;
|
||||||
|
if (useJsSandbox()) {
|
||||||
|
result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args);
|
||||||
|
} else {
|
||||||
|
result = ((Invocable)engine).invokeFunction(functionName, args);
|
||||||
|
}
|
||||||
|
return Futures.immediateFuture(result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet();
|
blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet();
|
||||||
return Futures.immediateFailedFuture(e);
|
return Futures.immediateFailedFuture(e);
|
||||||
@ -103,7 +125,11 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
|
|||||||
String functionName = functionsMap.get(scriptId);
|
String functionName = functionsMap.get(scriptId);
|
||||||
if (functionName != null) {
|
if (functionName != null) {
|
||||||
try {
|
try {
|
||||||
sandbox.eval(functionName + " = undefined;");
|
if (useJsSandbox()) {
|
||||||
|
sandbox.eval(functionName + " = undefined;");
|
||||||
|
} else {
|
||||||
|
engine.eval(functionName + " = undefined;");
|
||||||
|
}
|
||||||
functionsMap.remove(scriptId);
|
functionsMap.remove(scriptId);
|
||||||
blackListedFunctions.remove(scriptId);
|
blackListedFunctions.remove(scriptId);
|
||||||
} catch (ScriptException e) {
|
} catch (ScriptException e) {
|
||||||
|
|||||||
@ -24,6 +24,9 @@ import org.springframework.stereotype.Service;
|
|||||||
@Service
|
@Service
|
||||||
public class NashornJsSandboxService extends AbstractNashornJsSandboxService {
|
public class NashornJsSandboxService extends AbstractNashornJsSandboxService {
|
||||||
|
|
||||||
|
@Value("${actors.rule.js_sandbox.use_js_sandbox}")
|
||||||
|
private boolean useJsSandbox;
|
||||||
|
|
||||||
@Value("${actors.rule.js_sandbox.monitor_thread_pool_size}")
|
@Value("${actors.rule.js_sandbox.monitor_thread_pool_size}")
|
||||||
private int monitorThreadPoolSize;
|
private int monitorThreadPoolSize;
|
||||||
|
|
||||||
@ -33,6 +36,11 @@ public class NashornJsSandboxService extends AbstractNashornJsSandboxService {
|
|||||||
@Value("${actors.rule.js_sandbox.max_errors}")
|
@Value("${actors.rule.js_sandbox.max_errors}")
|
||||||
private int maxErrors;
|
private int maxErrors;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean useJsSandbox() {
|
||||||
|
return useJsSandbox;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getMonitorThreadPoolSize() {
|
protected int getMonitorThreadPoolSize() {
|
||||||
return monitorThreadPoolSize;
|
return monitorThreadPoolSize;
|
||||||
|
|||||||
@ -239,6 +239,8 @@ actors:
|
|||||||
# Specify thread pool size for external call service
|
# Specify thread pool size for external call service
|
||||||
external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:10}"
|
external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:10}"
|
||||||
js_sandbox:
|
js_sandbox:
|
||||||
|
# Use Sandboxed (secured) JavaScript environment
|
||||||
|
use_js_sandbox: "${ACTORS_RULE_JS_SANDBOX_USE_JS_SANDBOX:true}"
|
||||||
# Specify thread pool size for JavaScript sandbox resource monitor
|
# Specify thread pool size for JavaScript sandbox resource monitor
|
||||||
monitor_thread_pool_size: "${ACTORS_RULE_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}"
|
monitor_thread_pool_size: "${ACTORS_RULE_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}"
|
||||||
# Maximum CPU time in milliseconds allowed for script execution
|
# Maximum CPU time in milliseconds allowed for script execution
|
||||||
|
|||||||
@ -37,7 +37,7 @@ public class RuleNodeJsScriptEngineTest {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void beforeTest() throws Exception {
|
public void beforeTest() throws Exception {
|
||||||
jsSandboxService = new TestNashornJsSandboxService(1, 100, 3);
|
jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
|||||||
@ -27,17 +27,24 @@ import java.util.concurrent.Executors;
|
|||||||
|
|
||||||
public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService {
|
public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService {
|
||||||
|
|
||||||
|
private boolean useJsSandbox;
|
||||||
private final int monitorThreadPoolSize;
|
private final int monitorThreadPoolSize;
|
||||||
private final long maxCpuTime;
|
private final long maxCpuTime;
|
||||||
private final int maxErrors;
|
private final int maxErrors;
|
||||||
|
|
||||||
public TestNashornJsSandboxService(int monitorThreadPoolSize, long maxCpuTime, int maxErrors) {
|
public TestNashornJsSandboxService(boolean useJsSandbox, int monitorThreadPoolSize, long maxCpuTime, int maxErrors) {
|
||||||
|
this.useJsSandbox = useJsSandbox;
|
||||||
this.monitorThreadPoolSize = monitorThreadPoolSize;
|
this.monitorThreadPoolSize = monitorThreadPoolSize;
|
||||||
this.maxCpuTime = maxCpuTime;
|
this.maxCpuTime = maxCpuTime;
|
||||||
this.maxErrors = maxErrors;
|
this.maxErrors = maxErrors;
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean useJsSandbox() {
|
||||||
|
return useJsSandbox;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getMonitorThreadPoolSize() {
|
protected int getMonitorThreadPoolSize() {
|
||||||
return monitorThreadPoolSize;
|
return monitorThreadPoolSize;
|
||||||
|
|||||||
@ -30,6 +30,10 @@ import jsFuncTemplate from './js-func.tpl.html';
|
|||||||
|
|
||||||
/* eslint-enable import/no-unresolved, import/default */
|
/* eslint-enable import/no-unresolved, import/default */
|
||||||
|
|
||||||
|
import beautify from 'js-beautify';
|
||||||
|
|
||||||
|
const js_beautify = beautify.js;
|
||||||
|
|
||||||
/* eslint-disable angular/angularelement */
|
/* eslint-disable angular/angularelement */
|
||||||
|
|
||||||
export default angular.module('thingsboard.directives.jsFunc', [thingsboardToast, thingsboardUtils, thingsboardExpandFullscreen])
|
export default angular.module('thingsboard.directives.jsFunc', [thingsboardToast, thingsboardUtils, thingsboardExpandFullscreen])
|
||||||
@ -72,6 +76,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
|
|||||||
updateEditorSize();
|
updateEditorSize();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
scope.beautifyJs = function () {
|
||||||
|
var res = js_beautify(scope.functionBody, {indent_size: 4, wrap_line_length: 60});
|
||||||
|
scope.functionBody = res;
|
||||||
|
};
|
||||||
|
|
||||||
function updateEditorSize() {
|
function updateEditorSize() {
|
||||||
if (scope.js_editor) {
|
if (scope.js_editor) {
|
||||||
scope.js_editor.resize();
|
scope.js_editor.resize();
|
||||||
|
|||||||
@ -23,6 +23,19 @@ tb-js-func {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tb-js-func-toolbar {
|
||||||
|
.md-button.tidy {
|
||||||
|
color: #7B7B7B;
|
||||||
|
min-width: 32px;
|
||||||
|
min-height: 15px;
|
||||||
|
line-height: 15px;
|
||||||
|
font-size: 0.800rem;
|
||||||
|
margin: 0 5px 0 0;
|
||||||
|
padding: 4px;
|
||||||
|
background: rgba(220, 220, 220, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tb-js-func-panel {
|
.tb-js-func-panel {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
border: 1px solid #C0C0C0;
|
border: 1px solid #C0C0C0;
|
||||||
|
|||||||
@ -16,9 +16,12 @@
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
<div style="background: #fff;" ng-class="{'tb-disabled': disabled, 'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()">
|
<div style="background: #fff;" ng-class="{'tb-disabled': disabled, 'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()">
|
||||||
<div layout="row" layout-align="start center" style="height: 40px;">
|
<div layout="row" layout-align="start center" style="height: 40px;" class="tb-js-func-toolbar">
|
||||||
<label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label>
|
<label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label>
|
||||||
<span flex></span>
|
<span flex></span>
|
||||||
|
<md-button ng-if="!disabled" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJs()">{{
|
||||||
|
'js-func.tidy' | translate }}
|
||||||
|
</md-button>
|
||||||
<div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
|
<div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tb-javascript-panel" class="tb-js-func-panel">
|
<div id="tb-javascript-panel" class="tb-js-func-panel">
|
||||||
|
|||||||
@ -29,6 +29,10 @@ import jsonContentTemplate from './json-content.tpl.html';
|
|||||||
|
|
||||||
/* eslint-enable import/no-unresolved, import/default */
|
/* eslint-enable import/no-unresolved, import/default */
|
||||||
|
|
||||||
|
import beautify from 'js-beautify';
|
||||||
|
|
||||||
|
const js_beautify = beautify.js;
|
||||||
|
|
||||||
export default angular.module('thingsboard.directives.jsonContent', [])
|
export default angular.module('thingsboard.directives.jsonContent', [])
|
||||||
.directive('tbJsonContent', JsonContent)
|
.directive('tbJsonContent', JsonContent)
|
||||||
.name;
|
.name;
|
||||||
@ -52,6 +56,11 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
|
|||||||
updateEditorSize();
|
updateEditorSize();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
scope.beautifyJson = function () {
|
||||||
|
var res = js_beautify(scope.contentBody, {indent_size: 4, wrap_line_length: 60});
|
||||||
|
scope.contentBody = res;
|
||||||
|
};
|
||||||
|
|
||||||
function updateEditorSize() {
|
function updateEditorSize() {
|
||||||
if (scope.json_editor) {
|
if (scope.json_editor) {
|
||||||
scope.json_editor.resize();
|
scope.json_editor.resize();
|
||||||
|
|||||||
@ -20,6 +20,19 @@ tb-json-content {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tb-json-content-toolbar {
|
||||||
|
.md-button.tidy {
|
||||||
|
color: #7B7B7B;
|
||||||
|
min-width: 32px;
|
||||||
|
min-height: 15px;
|
||||||
|
line-height: 15px;
|
||||||
|
font-size: 0.800rem;
|
||||||
|
margin: 0 5px 0 0;
|
||||||
|
padding: 4px;
|
||||||
|
background: rgba(220, 220, 220, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tb-json-content-panel {
|
.tb-json-content-panel {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
border: 1px solid #C0C0C0;
|
border: 1px solid #C0C0C0;
|
||||||
|
|||||||
@ -16,9 +16,12 @@
|
|||||||
|
|
||||||
-->
|
-->
|
||||||
<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
|
<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
|
||||||
<div layout="row" layout-align="start center">
|
<div layout="row" layout-align="start center" style="height: 40px;" class="tb-json-content-toolbar">
|
||||||
<label class="tb-title no-padding">{{ label }}</label>
|
<label class="tb-title no-padding">{{ label }}</label>
|
||||||
<span flex></span>
|
<span flex></span>
|
||||||
|
<md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJson()">{{
|
||||||
|
'js-func.tidy' | translate }}
|
||||||
|
</md-button>
|
||||||
<md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
|
<md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
|
||||||
</div>
|
</div>
|
||||||
<div flex id="tb-json-panel" class="tb-json-content-panel" layout="column">
|
<div flex id="tb-json-panel" class="tb-json-content-panel" layout="column">
|
||||||
|
|||||||
@ -991,7 +991,8 @@ export default angular.module('thingsboard.locale', [])
|
|||||||
},
|
},
|
||||||
"js-func": {
|
"js-func": {
|
||||||
"no-return-error": "Function must return value!",
|
"no-return-error": "Function must return value!",
|
||||||
"return-type-mismatch": "Function must return value of '{{type}}' type!"
|
"return-type-mismatch": "Function must return value of '{{type}}' type!",
|
||||||
|
"tidy": "Tidy"
|
||||||
},
|
},
|
||||||
"key-val": {
|
"key-val": {
|
||||||
"key": "Key",
|
"key": "Key",
|
||||||
|
|||||||
@ -76,9 +76,12 @@ md-dialog.tb-node-script-test-dialog {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 0.800rem;
|
font-size: 0.800rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
top: 10px;
|
top: 13px;
|
||||||
right: 40px;
|
right: 40px;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
&.tb-js-function {
|
||||||
|
right: 80px;
|
||||||
|
}
|
||||||
label {
|
label {
|
||||||
color: #00acc1;
|
color: #00acc1;
|
||||||
background: rgba(220, 220, 220, 0.35);
|
background: rgba(220, 220, 220, 0.35);
|
||||||
|
|||||||
@ -73,7 +73,7 @@
|
|||||||
<div id="bottom_panel" class="tb-split tb-split-vertical">
|
<div id="bottom_panel" class="tb-split tb-split-vertical">
|
||||||
<div id="bottom_left_panel" class="tb-split tb-content">
|
<div id="bottom_left_panel" class="tb-split tb-content">
|
||||||
<div class="tb-resize-container">
|
<div class="tb-resize-container">
|
||||||
<div class="tb-editor-area-title-panel">
|
<div class="tb-editor-area-title-panel tb-js-function">
|
||||||
<label>{{ vm.functionTitle }}</label>
|
<label>{{ vm.functionTitle }}</label>
|
||||||
</div>
|
</div>
|
||||||
<ng-form name="funcBodyForm">
|
<ng-form name="funcBodyForm">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user