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 delight.nashornsandbox.NashornSandbox;
import delight.nashornsandbox.NashornSandboxes;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import java.util.Map;
import java.util.UUID;
@ -35,7 +38,8 @@ import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public abstract class AbstractNashornJsSandboxService implements JsSandboxService {
private NashornSandbox sandbox = NashornSandboxes.create();
private NashornSandbox sandbox;
private ScriptEngine engine;
private ExecutorService monitorExecutorService;
private Map<UUID, String> functionsMap = new ConcurrentHashMap<>();
@ -44,11 +48,17 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
@PostConstruct
public void init() {
monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize());
sandbox.setExecutor(monitorExecutorService);
sandbox.setMaxCPUTime(getMaxCpuTime());
sandbox.allowNoBraces(false);
sandbox.setMaxPreparedStatements(30);
if (useJsSandbox()) {
sandbox = NashornSandboxes.create();
monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize());
sandbox.setExecutor(monitorExecutorService);
sandbox.setMaxCPUTime(getMaxCpuTime());
sandbox.allowNoBraces(false);
sandbox.setMaxPreparedStatements(30);
} else {
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
engine = factory.getScriptEngine(new String[]{"--no-java"});
}
}
@PreDestroy
@ -58,6 +68,8 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
}
}
protected abstract boolean useJsSandbox();
protected abstract int getMonitorThreadPoolSize();
protected abstract long getMaxCpuTime();
@ -70,7 +82,11 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
String functionName = "invokeInternal_" + scriptId.toString().replace('-','_');
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
try {
sandbox.eval(jsScript);
if (useJsSandbox()) {
sandbox.eval(jsScript);
} else {
engine.eval(jsScript);
}
functionsMap.put(scriptId, functionName);
} catch (Exception e) {
log.warn("Failed to compile JS script: {}", e.getMessage(), e);
@ -87,7 +103,13 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
}
if (!isBlackListed(scriptId)) {
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) {
blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet();
return Futures.immediateFailedFuture(e);
@ -103,7 +125,11 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
String functionName = functionsMap.get(scriptId);
if (functionName != null) {
try {
sandbox.eval(functionName + " = undefined;");
if (useJsSandbox()) {
sandbox.eval(functionName + " = undefined;");
} else {
engine.eval(functionName + " = undefined;");
}
functionsMap.remove(scriptId);
blackListedFunctions.remove(scriptId);
} catch (ScriptException e) {

View File

@ -24,6 +24,9 @@ import org.springframework.stereotype.Service;
@Service
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}")
private int monitorThreadPoolSize;
@ -33,6 +36,11 @@ public class NashornJsSandboxService extends AbstractNashornJsSandboxService {
@Value("${actors.rule.js_sandbox.max_errors}")
private int maxErrors;
@Override
protected boolean useJsSandbox() {
return useJsSandbox;
}
@Override
protected int getMonitorThreadPoolSize() {
return monitorThreadPoolSize;

View File

@ -239,6 +239,8 @@ actors:
# Specify thread pool size for external call service
external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:10}"
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
monitor_thread_pool_size: "${ACTORS_RULE_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}"
# Maximum CPU time in milliseconds allowed for script execution

View File

@ -37,7 +37,7 @@ public class RuleNodeJsScriptEngineTest {
@Before
public void beforeTest() throws Exception {
jsSandboxService = new TestNashornJsSandboxService(1, 100, 3);
jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3);
}
@After

View File

@ -27,17 +27,24 @@ import java.util.concurrent.Executors;
public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService {
private boolean useJsSandbox;
private final int monitorThreadPoolSize;
private final long maxCpuTime;
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.maxCpuTime = maxCpuTime;
this.maxErrors = maxErrors;
init();
}
@Override
protected boolean useJsSandbox() {
return useJsSandbox;
}
@Override
protected int getMonitorThreadPoolSize() {
return monitorThreadPoolSize;

View File

@ -30,6 +30,10 @@ import jsFuncTemplate from './js-func.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import beautify from 'js-beautify';
const js_beautify = beautify.js;
/* eslint-disable angular/angularelement */
export default angular.module('thingsboard.directives.jsFunc', [thingsboardToast, thingsboardUtils, thingsboardExpandFullscreen])
@ -72,6 +76,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
updateEditorSize();
};
scope.beautifyJs = function () {
var res = js_beautify(scope.functionBody, {indent_size: 4, wrap_line_length: 60});
scope.functionBody = res;
};
function updateEditorSize() {
if (scope.js_editor) {
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 {
margin-left: 15px;
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 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>
<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>
<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 */
import beautify from 'js-beautify';
const js_beautify = beautify.js;
export default angular.module('thingsboard.directives.jsonContent', [])
.directive('tbJsonContent', JsonContent)
.name;
@ -52,6 +56,11 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
updateEditorSize();
};
scope.beautifyJson = function () {
var res = js_beautify(scope.contentBody, {indent_size: 4, wrap_line_length: 60});
scope.contentBody = res;
};
function updateEditorSize() {
if (scope.json_editor) {
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 {
margin-left: 15px;
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 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>
<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>
</div>
<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": {
"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": "Key",

View File

@ -76,9 +76,12 @@ md-dialog.tb-node-script-test-dialog {
position: absolute;
font-size: 0.800rem;
font-weight: 500;
top: 10px;
top: 13px;
right: 40px;
z-index: 5;
&.tb-js-function {
right: 80px;
}
label {
color: #00acc1;
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_left_panel" class="tb-split tb-content">
<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>
</div>
<ng-form name="funcBodyForm">