Commit efbc65e11fdd81a5f3485552f3eeaa9ae4d2871b

Authored by Igor Kulikov
1 parent af5791ab

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

@@ -20,10 +20,13 @@ import com.google.common.util.concurrent.Futures; @@ -20,10 +20,13 @@ import com.google.common.util.concurrent.Futures;
20 import com.google.common.util.concurrent.ListenableFuture; 20 import com.google.common.util.concurrent.ListenableFuture;
21 import delight.nashornsandbox.NashornSandbox; 21 import delight.nashornsandbox.NashornSandbox;
22 import delight.nashornsandbox.NashornSandboxes; 22 import delight.nashornsandbox.NashornSandboxes;
  23 +import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
23 import lombok.extern.slf4j.Slf4j; 24 import lombok.extern.slf4j.Slf4j;
24 25
25 import javax.annotation.PostConstruct; 26 import javax.annotation.PostConstruct;
26 import javax.annotation.PreDestroy; 27 import javax.annotation.PreDestroy;
  28 +import javax.script.Invocable;
  29 +import javax.script.ScriptEngine;
27 import javax.script.ScriptException; 30 import javax.script.ScriptException;
28 import java.util.Map; 31 import java.util.Map;
29 import java.util.UUID; 32 import java.util.UUID;
@@ -35,7 +38,8 @@ import java.util.concurrent.atomic.AtomicInteger; @@ -35,7 +38,8 @@ import java.util.concurrent.atomic.AtomicInteger;
35 @Slf4j 38 @Slf4j
36 public abstract class AbstractNashornJsSandboxService implements JsSandboxService { 39 public abstract class AbstractNashornJsSandboxService implements JsSandboxService {
37 40
38 - private NashornSandbox sandbox = NashornSandboxes.create(); 41 + private NashornSandbox sandbox;
  42 + private ScriptEngine engine;
39 private ExecutorService monitorExecutorService; 43 private ExecutorService monitorExecutorService;
40 44
41 private Map<UUID, String> functionsMap = new ConcurrentHashMap<>(); 45 private Map<UUID, String> functionsMap = new ConcurrentHashMap<>();
@@ -44,11 +48,17 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @@ -44,11 +48,17 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
44 48
45 @PostConstruct 49 @PostConstruct
46 public void init() { 50 public void init() {
47 - monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize());  
48 - sandbox.setExecutor(monitorExecutorService);  
49 - sandbox.setMaxCPUTime(getMaxCpuTime());  
50 - sandbox.allowNoBraces(false);  
51 - sandbox.setMaxPreparedStatements(30); 51 + if (useJsSandbox()) {
  52 + sandbox = NashornSandboxes.create();
  53 + monitorExecutorService = Executors.newFixedThreadPool(getMonitorThreadPoolSize());
  54 + sandbox.setExecutor(monitorExecutorService);
  55 + sandbox.setMaxCPUTime(getMaxCpuTime());
  56 + sandbox.allowNoBraces(false);
  57 + sandbox.setMaxPreparedStatements(30);
  58 + } else {
  59 + NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
  60 + engine = factory.getScriptEngine(new String[]{"--no-java"});
  61 + }
52 } 62 }
53 63
54 @PreDestroy 64 @PreDestroy
@@ -58,6 +68,8 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @@ -58,6 +68,8 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
58 } 68 }
59 } 69 }
60 70
  71 + protected abstract boolean useJsSandbox();
  72 +
61 protected abstract int getMonitorThreadPoolSize(); 73 protected abstract int getMonitorThreadPoolSize();
62 74
63 protected abstract long getMaxCpuTime(); 75 protected abstract long getMaxCpuTime();
@@ -70,7 +82,11 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @@ -70,7 +82,11 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
70 String functionName = "invokeInternal_" + scriptId.toString().replace('-','_'); 82 String functionName = "invokeInternal_" + scriptId.toString().replace('-','_');
71 String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames); 83 String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
72 try { 84 try {
73 - sandbox.eval(jsScript); 85 + if (useJsSandbox()) {
  86 + sandbox.eval(jsScript);
  87 + } else {
  88 + engine.eval(jsScript);
  89 + }
74 functionsMap.put(scriptId, functionName); 90 functionsMap.put(scriptId, functionName);
75 } catch (Exception e) { 91 } catch (Exception e) {
76 log.warn("Failed to compile JS script: {}", e.getMessage(), e); 92 log.warn("Failed to compile JS script: {}", e.getMessage(), e);
@@ -87,7 +103,13 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @@ -87,7 +103,13 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
87 } 103 }
88 if (!isBlackListed(scriptId)) { 104 if (!isBlackListed(scriptId)) {
89 try { 105 try {
90 - return Futures.immediateFuture(sandbox.getSandboxedInvocable().invokeFunction(functionName, args)); 106 + Object result;
  107 + if (useJsSandbox()) {
  108 + result = sandbox.getSandboxedInvocable().invokeFunction(functionName, args);
  109 + } else {
  110 + result = ((Invocable)engine).invokeFunction(functionName, args);
  111 + }
  112 + return Futures.immediateFuture(result);
91 } catch (Exception e) { 113 } catch (Exception e) {
92 blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet(); 114 blackListedFunctions.computeIfAbsent(scriptId, key -> new AtomicInteger(0)).incrementAndGet();
93 return Futures.immediateFailedFuture(e); 115 return Futures.immediateFailedFuture(e);
@@ -103,7 +125,11 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic @@ -103,7 +125,11 @@ public abstract class AbstractNashornJsSandboxService implements JsSandboxServic
103 String functionName = functionsMap.get(scriptId); 125 String functionName = functionsMap.get(scriptId);
104 if (functionName != null) { 126 if (functionName != null) {
105 try { 127 try {
106 - sandbox.eval(functionName + " = undefined;"); 128 + if (useJsSandbox()) {
  129 + sandbox.eval(functionName + " = undefined;");
  130 + } else {
  131 + engine.eval(functionName + " = undefined;");
  132 + }
107 functionsMap.remove(scriptId); 133 functionsMap.remove(scriptId);
108 blackListedFunctions.remove(scriptId); 134 blackListedFunctions.remove(scriptId);
109 } catch (ScriptException e) { 135 } catch (ScriptException e) {
@@ -24,6 +24,9 @@ import org.springframework.stereotype.Service; @@ -24,6 +24,9 @@ import org.springframework.stereotype.Service;
24 @Service 24 @Service
25 public class NashornJsSandboxService extends AbstractNashornJsSandboxService { 25 public class NashornJsSandboxService extends AbstractNashornJsSandboxService {
26 26
  27 + @Value("${actors.rule.js_sandbox.use_js_sandbox}")
  28 + private boolean useJsSandbox;
  29 +
27 @Value("${actors.rule.js_sandbox.monitor_thread_pool_size}") 30 @Value("${actors.rule.js_sandbox.monitor_thread_pool_size}")
28 private int monitorThreadPoolSize; 31 private int monitorThreadPoolSize;
29 32
@@ -34,6 +37,11 @@ public class NashornJsSandboxService extends AbstractNashornJsSandboxService { @@ -34,6 +37,11 @@ public class NashornJsSandboxService extends AbstractNashornJsSandboxService {
34 private int maxErrors; 37 private int maxErrors;
35 38
36 @Override 39 @Override
  40 + protected boolean useJsSandbox() {
  41 + return useJsSandbox;
  42 + }
  43 +
  44 + @Override
37 protected int getMonitorThreadPoolSize() { 45 protected int getMonitorThreadPoolSize() {
38 return monitorThreadPoolSize; 46 return monitorThreadPoolSize;
39 } 47 }
@@ -239,6 +239,8 @@ actors: @@ -239,6 +239,8 @@ actors:
239 # Specify thread pool size for external call service 239 # Specify thread pool size for external call service
240 external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:10}" 240 external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:10}"
241 js_sandbox: 241 js_sandbox:
  242 + # Use Sandboxed (secured) JavaScript environment
  243 + use_js_sandbox: "${ACTORS_RULE_JS_SANDBOX_USE_JS_SANDBOX:true}"
242 # Specify thread pool size for JavaScript sandbox resource monitor 244 # Specify thread pool size for JavaScript sandbox resource monitor
243 monitor_thread_pool_size: "${ACTORS_RULE_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}" 245 monitor_thread_pool_size: "${ACTORS_RULE_JS_SANDBOX_MONITOR_THREAD_POOL_SIZE:4}"
244 # Maximum CPU time in milliseconds allowed for script execution 246 # Maximum CPU time in milliseconds allowed for script execution
@@ -37,7 +37,7 @@ public class RuleNodeJsScriptEngineTest { @@ -37,7 +37,7 @@ public class RuleNodeJsScriptEngineTest {
37 37
38 @Before 38 @Before
39 public void beforeTest() throws Exception { 39 public void beforeTest() throws Exception {
40 - jsSandboxService = new TestNashornJsSandboxService(1, 100, 3); 40 + jsSandboxService = new TestNashornJsSandboxService(false, 1, 100, 3);
41 } 41 }
42 42
43 @After 43 @After
@@ -27,11 +27,13 @@ import java.util.concurrent.Executors; @@ -27,11 +27,13 @@ import java.util.concurrent.Executors;
27 27
28 public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService { 28 public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService {
29 29
  30 + private boolean useJsSandbox;
30 private final int monitorThreadPoolSize; 31 private final int monitorThreadPoolSize;
31 private final long maxCpuTime; 32 private final long maxCpuTime;
32 private final int maxErrors; 33 private final int maxErrors;
33 34
34 - public TestNashornJsSandboxService(int monitorThreadPoolSize, long maxCpuTime, int maxErrors) { 35 + public TestNashornJsSandboxService(boolean useJsSandbox, int monitorThreadPoolSize, long maxCpuTime, int maxErrors) {
  36 + this.useJsSandbox = useJsSandbox;
35 this.monitorThreadPoolSize = monitorThreadPoolSize; 37 this.monitorThreadPoolSize = monitorThreadPoolSize;
36 this.maxCpuTime = maxCpuTime; 38 this.maxCpuTime = maxCpuTime;
37 this.maxErrors = maxErrors; 39 this.maxErrors = maxErrors;
@@ -39,6 +41,11 @@ public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService @@ -39,6 +41,11 @@ public class TestNashornJsSandboxService extends AbstractNashornJsSandboxService
39 } 41 }
40 42
41 @Override 43 @Override
  44 + protected boolean useJsSandbox() {
  45 + return useJsSandbox;
  46 + }
  47 +
  48 + @Override
42 protected int getMonitorThreadPoolSize() { 49 protected int getMonitorThreadPoolSize() {
43 return monitorThreadPoolSize; 50 return monitorThreadPoolSize;
44 } 51 }
@@ -30,6 +30,10 @@ import jsFuncTemplate from './js-func.tpl.html'; @@ -30,6 +30,10 @@ import jsFuncTemplate from './js-func.tpl.html';
30 30
31 /* eslint-enable import/no-unresolved, import/default */ 31 /* eslint-enable import/no-unresolved, import/default */
32 32
  33 +import beautify from 'js-beautify';
  34 +
  35 +const js_beautify = beautify.js;
  36 +
33 /* eslint-disable angular/angularelement */ 37 /* eslint-disable angular/angularelement */
34 38
35 export default angular.module('thingsboard.directives.jsFunc', [thingsboardToast, thingsboardUtils, thingsboardExpandFullscreen]) 39 export default angular.module('thingsboard.directives.jsFunc', [thingsboardToast, thingsboardUtils, thingsboardExpandFullscreen])
@@ -72,6 +76,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) { @@ -72,6 +76,11 @@ function JsFunc($compile, $templateCache, toast, utils, $translate) {
72 updateEditorSize(); 76 updateEditorSize();
73 }; 77 };
74 78
  79 + scope.beautifyJs = function () {
  80 + var res = js_beautify(scope.functionBody, {indent_size: 4, wrap_line_length: 60});
  81 + scope.functionBody = res;
  82 + };
  83 +
75 function updateEditorSize() { 84 function updateEditorSize() {
76 if (scope.js_editor) { 85 if (scope.js_editor) {
77 scope.js_editor.resize(); 86 scope.js_editor.resize();
@@ -23,6 +23,19 @@ tb-js-func { @@ -23,6 +23,19 @@ tb-js-func {
23 } 23 }
24 } 24 }
25 25
  26 +.tb-js-func-toolbar {
  27 + .md-button.tidy {
  28 + color: #7B7B7B;
  29 + min-width: 32px;
  30 + min-height: 15px;
  31 + line-height: 15px;
  32 + font-size: 0.800rem;
  33 + margin: 0 5px 0 0;
  34 + padding: 4px;
  35 + background: rgba(220, 220, 220, 0.35);
  36 + }
  37 +}
  38 +
26 .tb-js-func-panel { 39 .tb-js-func-panel {
27 margin-left: 15px; 40 margin-left: 15px;
28 border: 1px solid #C0C0C0; 41 border: 1px solid #C0C0C0;
@@ -16,9 +16,12 @@ @@ -16,9 +16,12 @@
16 16
17 --> 17 -->
18 <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()"> 18 <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()">
19 - <div layout="row" layout-align="start center" style="height: 40px;"> 19 + <div layout="row" layout-align="start center" style="height: 40px;" class="tb-js-func-toolbar">
20 <label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label> 20 <label class="tb-title no-padding">function {{ functionName }}({{ functionArgsString }}) {</label>
21 <span flex></span> 21 <span flex></span>
  22 + <md-button ng-if="!disabled" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJs()">{{
  23 + 'js-func.tidy' | translate }}
  24 + </md-button>
22 <div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div> 25 <div id="expand-button" layout="column" aria-label="Fullscreen" class="md-button md-icon-button tb-md-32 tb-fullscreen-button-style"></div>
23 </div> 26 </div>
24 <div id="tb-javascript-panel" class="tb-js-func-panel"> 27 <div id="tb-javascript-panel" class="tb-js-func-panel">
@@ -29,6 +29,10 @@ import jsonContentTemplate from './json-content.tpl.html'; @@ -29,6 +29,10 @@ import jsonContentTemplate from './json-content.tpl.html';
29 29
30 /* eslint-enable import/no-unresolved, import/default */ 30 /* eslint-enable import/no-unresolved, import/default */
31 31
  32 +import beautify from 'js-beautify';
  33 +
  34 +const js_beautify = beautify.js;
  35 +
32 export default angular.module('thingsboard.directives.jsonContent', []) 36 export default angular.module('thingsboard.directives.jsonContent', [])
33 .directive('tbJsonContent', JsonContent) 37 .directive('tbJsonContent', JsonContent)
34 .name; 38 .name;
@@ -52,6 +56,11 @@ function JsonContent($compile, $templateCache, toast, types, utils) { @@ -52,6 +56,11 @@ function JsonContent($compile, $templateCache, toast, types, utils) {
52 updateEditorSize(); 56 updateEditorSize();
53 }; 57 };
54 58
  59 + scope.beautifyJson = function () {
  60 + var res = js_beautify(scope.contentBody, {indent_size: 4, wrap_line_length: 60});
  61 + scope.contentBody = res;
  62 + };
  63 +
55 function updateEditorSize() { 64 function updateEditorSize() {
56 if (scope.json_editor) { 65 if (scope.json_editor) {
57 scope.json_editor.resize(); 66 scope.json_editor.resize();
@@ -20,6 +20,19 @@ tb-json-content { @@ -20,6 +20,19 @@ tb-json-content {
20 } 20 }
21 } 21 }
22 22
  23 +.tb-json-content-toolbar {
  24 + .md-button.tidy {
  25 + color: #7B7B7B;
  26 + min-width: 32px;
  27 + min-height: 15px;
  28 + line-height: 15px;
  29 + font-size: 0.800rem;
  30 + margin: 0 5px 0 0;
  31 + padding: 4px;
  32 + background: rgba(220, 220, 220, 0.35);
  33 + }
  34 +}
  35 +
23 .tb-json-content-panel { 36 .tb-json-content-panel {
24 margin-left: 15px; 37 margin-left: 15px;
25 border: 1px solid #C0C0C0; 38 border: 1px solid #C0C0C0;
@@ -16,9 +16,12 @@ @@ -16,9 +16,12 @@
16 16
17 --> 17 -->
18 <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"> 18 <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">
19 - <div layout="row" layout-align="start center"> 19 + <div layout="row" layout-align="start center" style="height: 40px;" class="tb-json-content-toolbar">
20 <label class="tb-title no-padding">{{ label }}</label> 20 <label class="tb-title no-padding">{{ label }}</label>
21 <span flex></span> 21 <span flex></span>
  22 + <md-button ng-if="!readonly" class="tidy" aria-label="{{ 'js-func.tidy' | translate }}" ng-click="beautifyJson()">{{
  23 + 'js-func.tidy' | translate }}
  24 + </md-button>
22 <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button> 25 <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
23 </div> 26 </div>
24 <div flex id="tb-json-panel" class="tb-json-content-panel" layout="column"> 27 <div flex id="tb-json-panel" class="tb-json-content-panel" layout="column">
@@ -991,7 +991,8 @@ export default angular.module('thingsboard.locale', []) @@ -991,7 +991,8 @@ export default angular.module('thingsboard.locale', [])
991 }, 991 },
992 "js-func": { 992 "js-func": {
993 "no-return-error": "Function must return value!", 993 "no-return-error": "Function must return value!",
994 - "return-type-mismatch": "Function must return value of '{{type}}' type!" 994 + "return-type-mismatch": "Function must return value of '{{type}}' type!",
  995 + "tidy": "Tidy"
995 }, 996 },
996 "key-val": { 997 "key-val": {
997 "key": "Key", 998 "key": "Key",
@@ -76,9 +76,12 @@ md-dialog.tb-node-script-test-dialog { @@ -76,9 +76,12 @@ md-dialog.tb-node-script-test-dialog {
76 position: absolute; 76 position: absolute;
77 font-size: 0.800rem; 77 font-size: 0.800rem;
78 font-weight: 500; 78 font-weight: 500;
79 - top: 10px; 79 + top: 13px;
80 right: 40px; 80 right: 40px;
81 z-index: 5; 81 z-index: 5;
  82 + &.tb-js-function {
  83 + right: 80px;
  84 + }
82 label { 85 label {
83 color: #00acc1; 86 color: #00acc1;
84 background: rgba(220, 220, 220, 0.35); 87 background: rgba(220, 220, 220, 0.35);
@@ -73,7 +73,7 @@ @@ -73,7 +73,7 @@
73 <div id="bottom_panel" class="tb-split tb-split-vertical"> 73 <div id="bottom_panel" class="tb-split tb-split-vertical">
74 <div id="bottom_left_panel" class="tb-split tb-content"> 74 <div id="bottom_left_panel" class="tb-split tb-content">
75 <div class="tb-resize-container"> 75 <div class="tb-resize-container">
76 - <div class="tb-editor-area-title-panel"> 76 + <div class="tb-editor-area-title-panel tb-js-function">
77 <label>{{ vm.functionTitle }}</label> 77 <label>{{ vm.functionTitle }}</label>
78 </div> 78 </div>
79 <ng-form name="funcBodyForm"> 79 <ng-form name="funcBodyForm">