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