Enable/Disable Sandboxed JavaScript environment. UI: Tidy button to format java scripts.

This commit is contained in:
Igor Kulikov 2018-05-18 19:41:02 +03:00
parent af5791ab7a
commit efbc65e11f
14 changed files with 113 additions and 16 deletions

View File

@ -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() {
if (useJsSandbox()) {
sandbox = NashornSandboxes.create();
monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize()); monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize());
sandbox.setExecutor(monitorExecutorService); sandbox.setExecutor(monitorExecutorService);
sandbox.setMaxCPUTime(getMaxCpuTime()); sandbox.setMaxCPUTime(getMaxCpuTime());
sandbox.allowNoBraces(false); sandbox.allowNoBraces(false);
sandbox.setMaxPreparedStatements(30); 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 {
if (useJsSandbox()) {
sandbox.eval(jsScript); 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 {
if (useJsSandbox()) {
sandbox.eval(functionName + " = undefined;"); 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) {

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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();

View File

@ -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;

View File

@ -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">

View File

@ -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();

View File

@ -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;

View File

@ -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">

View File

@ -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",

View File

@ -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);

View File

@ -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">