From 04432ee73fdd612e75c1fe388e7039c6b768e8d6 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 17 May 2018 19:21:34 +0300 Subject: [PATCH] JavaScript sandbox service. --- application/pom.xml | 4 ++ .../server/actors/ActorSystemContext.java | 5 ++ .../actors/ruleChain/DefaultTbContext.java | 4 +- .../controller/RuleChainController.java | 9 ++- .../executors/AbstractListeningExecutor.java | 4 ++ .../service/script/JsSandboxService.java | 27 +++++++ ...shornJsEngine.java => JsScriptEngine.java} | 21 +++--- .../script/NashornJsSandboxService.java | 70 +++++++++++++++++++ .../src/main/resources/thingsboard.yml | 5 ++ ...ngineTest.java => JsScriptEngineTest.java} | 31 +++++--- .../script/TestNashornJsSandboxService.java | 54 ++++++++++++++ pom.xml | 6 ++ 12 files changed, 218 insertions(+), 22 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java rename application/src/main/java/org/thingsboard/server/service/script/{NashornJsEngine.java => JsScriptEngine.java} (90%) create mode 100644 application/src/main/java/org/thingsboard/server/service/script/NashornJsSandboxService.java rename application/src/test/java/org/thingsboard/server/service/script/{NashornJsEngineTest.java => JsScriptEngineTest.java} (85%) create mode 100644 application/src/test/java/org/thingsboard/server/service/script/TestNashornJsSandboxService.java diff --git a/application/pom.xml b/application/pom.xml index 45e6f23845..e0b6b98d9a 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -256,6 +256,10 @@ org.hsqldb hsqldb + + org.javadelight + delight-nashorn-sandbox + diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 6986f628a6..f7c7f1a5a8 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -62,6 +62,7 @@ import org.thingsboard.server.service.mail.MailExecutorService; import org.thingsboard.server.service.queue.MsgQueueService; import org.thingsboard.server.service.rpc.DeviceRpcService; import org.thingsboard.server.service.script.JsExecutorService; +import org.thingsboard.server.service.script.JsSandboxService; import org.thingsboard.server.service.state.DeviceStateService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; @@ -162,6 +163,10 @@ public class ActorSystemContext { @Getter private DeviceRpcService deviceRpcService; + @Autowired + @Getter + private JsSandboxService jsSandbox; + @Autowired @Getter private JsExecutorService jsExecutor; diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 6501fdc464..a8b66dc2f9 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -44,7 +44,7 @@ import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.service.script.NashornJsEngine; +import org.thingsboard.server.service.script.JsScriptEngine; import scala.concurrent.duration.Duration; import java.util.Collections; @@ -152,7 +152,7 @@ class DefaultTbContext implements TbContext { @Override public ScriptEngine createJsScriptEngine(String script, String functionName, String... argNames) { - return new NashornJsEngine(script, functionName, argNames); + return new JsScriptEngine(mainCtx.getJsSandbox(), script, functionName, argNames); } @Override diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java index 68482b3cfb..e8a5f8dcc0 100644 --- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java +++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java @@ -50,7 +50,9 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.event.EventService; -import org.thingsboard.server.service.script.NashornJsEngine; +import org.thingsboard.server.service.script.JsExecutorService; +import org.thingsboard.server.service.script.JsSandboxService; +import org.thingsboard.server.service.script.JsScriptEngine; import java.util.List; import java.util.Map; @@ -69,6 +71,9 @@ public class RuleChainController extends BaseController { @Autowired private EventService eventService; + @Autowired + private JsSandboxService jsSandboxService; + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')") @RequestMapping(value = "/ruleChain/{ruleChainId}", method = RequestMethod.GET) @ResponseBody @@ -273,7 +278,7 @@ public class RuleChainController extends BaseController { String errorText = ""; ScriptEngine engine = null; try { - engine = new NashornJsEngine(script, functionName, argNames); + engine = new JsScriptEngine(jsSandboxService, script, functionName, argNames); TbMsg inMsg = new TbMsg(UUIDs.timeBased(), msgType, null, new TbMsgMetaData(metadata), data, null, null, 0L); switch (scriptType) { case "update": diff --git a/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java b/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java index 91ef9de66f..210d9da27c 100644 --- a/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java +++ b/application/src/main/java/org/thingsboard/server/service/executors/AbstractListeningExecutor.java @@ -54,6 +54,10 @@ public abstract class AbstractListeningExecutor implements ListeningExecutor { service.execute(command); } + public ListeningExecutorService executor() { + return service; + } + protected abstract int getThreadPollSize(); } diff --git a/application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java b/application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java new file mode 100644 index 0000000000..d25085730a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/script/JsSandboxService.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.thingsboard.server.service.script; + +import javax.script.ScriptException; + +public interface JsSandboxService { + + Object eval(String js) throws ScriptException; + + Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/script/NashornJsEngine.java b/application/src/main/java/org/thingsboard/server/service/script/JsScriptEngine.java similarity index 90% rename from application/src/main/java/org/thingsboard/server/service/script/NashornJsEngine.java rename to application/src/main/java/org/thingsboard/server/service/script/JsScriptEngine.java index b5cc5e036c..5a4c35461d 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/NashornJsEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/JsScriptEngine.java @@ -19,14 +19,11 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Sets; -import jdk.nashorn.api.scripting.NashornScriptEngineFactory; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import javax.script.Invocable; -import javax.script.ScriptEngine; import javax.script.ScriptException; import java.util.Collections; import java.util.Map; @@ -34,7 +31,7 @@ import java.util.Set; @Slf4j -public class NashornJsEngine implements org.thingsboard.rule.engine.api.ScriptEngine { +public class JsScriptEngine implements org.thingsboard.rule.engine.api.ScriptEngine { public static final String MSG = "msg"; public static final String METADATA = "metadata"; @@ -49,12 +46,14 @@ public class NashornJsEngine implements org.thingsboard.rule.engine.api.ScriptEn "\n}"; private static final ObjectMapper mapper = new ObjectMapper(); - private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); - private ScriptEngine engine = factory.getScriptEngine(new String[]{"--no-java"}); +// private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory(); +// private ScriptEngine engine = factory.getScriptEngine(new String[]{"--no-java"}); + private final JsSandboxService sandboxService; private final String invokeFunctionName; - public NashornJsEngine(String script, String functionName, String... argNames) { + public JsScriptEngine(JsSandboxService sandboxService, String script, String functionName, String... argNames) { + this.sandboxService = sandboxService; this.invokeFunctionName = "invokeInternal" + this.hashCode(); String msgArg; String metadataArg; @@ -75,7 +74,8 @@ public class NashornJsEngine implements org.thingsboard.rule.engine.api.ScriptEn private void compileScript(String script) { try { - engine.eval(script); + //engine.eval(script); + sandboxService.eval(script); } catch (ScriptException e) { log.warn("Failed to compile JS script: {}", e.getMessage(), e); throw new IllegalArgumentException("Can't compile script: " + e.getMessage()); @@ -195,7 +195,8 @@ public class NashornJsEngine implements org.thingsboard.rule.engine.api.ScriptEn private JsonNode executeScript(TbMsg msg) throws ScriptException { try { String[] inArgs = prepareArgs(msg); - String eval = ((Invocable)engine).invokeFunction(this.invokeFunctionName, inArgs[0], inArgs[1], inArgs[2]).toString(); + //String eval = ((Invocable)engine).invokeFunction(this.invokeFunctionName, inArgs[0], inArgs[1], inArgs[2]).toString(); + String eval = sandboxService.invokeFunction(this.invokeFunctionName, inArgs[0], inArgs[1], inArgs[2]).toString(); return mapper.readTree(eval); } catch (ScriptException | IllegalArgumentException th) { throw th; @@ -206,6 +207,6 @@ public class NashornJsEngine implements org.thingsboard.rule.engine.api.ScriptEn } public void destroy() { - engine = null; + //engine = null; } } diff --git a/application/src/main/java/org/thingsboard/server/service/script/NashornJsSandboxService.java b/application/src/main/java/org/thingsboard/server/service/script/NashornJsSandboxService.java new file mode 100644 index 0000000000..576e1495a3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/script/NashornJsSandboxService.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.thingsboard.server.service.script; + +import delight.nashornsandbox.NashornSandbox; +import delight.nashornsandbox.NashornSandboxes; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.script.ScriptException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Slf4j +@Service +public class NashornJsSandboxService implements JsSandboxService { + + @Value("${actors.rule.js_sandbox.monitor_thread_pool_size}") + private int monitorThreadPoolSize; + + @Value("${actors.rule.js_sandbox.max_cpu_time}") + private long maxCpuTime; + + private NashornSandbox sandbox = NashornSandboxes.create(); + private ExecutorService monitorExecutorService; + + @PostConstruct + public void init() { + monitorExecutorService = Executors.newFixedThreadPool(monitorThreadPoolSize); + sandbox.setExecutor(monitorExecutorService); + sandbox.setMaxCPUTime(maxCpuTime); + sandbox.allowNoBraces(false); + sandbox.setMaxPreparedStatements(30); + } + + @PreDestroy + public void stop() { + if (monitorExecutorService != null) { + monitorExecutorService.shutdownNow(); + } + } + + @Override + public Object eval(String js) throws ScriptException { + return sandbox.eval(js); + } + + @Override + public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { + return sandbox.getSandboxedInvocable().invokeFunction(name, args); + } + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 3d50258d39..50f6e3e0b5 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -238,6 +238,11 @@ actors: mail_thread_pool_size: "${ACTORS_RULE_MAIL_THREAD_POOL_SIZE:10}" # Specify thread pool size for external call service external_call_thread_pool_size: "${ACTORS_RULE_EXTERNAL_CALL_THREAD_POOL_SIZE:10}" + js_sandbox: + # 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 + max_cpu_time: "${ACTORS_RULE_JS_SANDBOX_MAX_CPU_TIME:100}" chain: # Errors for particular actor are persisted once per specified amount of milliseconds error_persist_frequency: "${ACTORS_RULE_CHAIN_ERROR_FREQUENCY:3000}" diff --git a/application/src/test/java/org/thingsboard/server/service/script/NashornJsEngineTest.java b/application/src/test/java/org/thingsboard/server/service/script/JsScriptEngineTest.java similarity index 85% rename from application/src/test/java/org/thingsboard/server/service/script/NashornJsEngineTest.java rename to application/src/test/java/org/thingsboard/server/service/script/JsScriptEngineTest.java index 1981287f6d..13230e5a93 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/NashornJsEngineTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/JsScriptEngineTest.java @@ -17,6 +17,8 @@ package org.thingsboard.server.service.script; import com.datastax.driver.core.utils.UUIDs; import com.google.common.collect.Sets; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.server.common.msg.TbMsg; @@ -25,17 +27,30 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import javax.script.ScriptException; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import static org.junit.Assert.*; -public class NashornJsEngineTest { +public class JsScriptEngineTest { private ScriptEngine scriptEngine; + private TestNashornJsSandboxService jsSandboxService; + + @Before + public void beforeTest() throws Exception { + jsSandboxService = new TestNashornJsSandboxService(1, 100); + } + + @After + public void afterTest() throws Exception { + jsSandboxService.destroy(); + } @Test public void msgCanBeUpdated() throws ScriptException { String function = "metadata.temp = metadata.temp * 10; return {metadata: metadata};"; - scriptEngine = new NashornJsEngine(function, "Transform"); + scriptEngine = new JsScriptEngine(jsSandboxService, function, "Transform"); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); @@ -51,7 +66,7 @@ public class NashornJsEngineTest { @Test public void newAttributesCanBeAddedInMsg() throws ScriptException { String function = "metadata.newAttr = metadata.humidity - msg.passed; return {metadata: metadata};"; - scriptEngine = new NashornJsEngine(function, "Transform"); + scriptEngine = new JsScriptEngine(jsSandboxService, function, "Transform"); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); metaData.putValue("humidity", "99"); @@ -66,7 +81,7 @@ public class NashornJsEngineTest { @Test public void payloadCanBeUpdated() throws ScriptException { String function = "msg.passed = msg.passed * metadata.temp; msg.bigObj.newProp = 'Ukraine'; return {msg: msg};"; - scriptEngine = new NashornJsEngine(function, "Transform"); + scriptEngine = new JsScriptEngine(jsSandboxService, function, "Transform"); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); metaData.putValue("humidity", "99"); @@ -83,7 +98,7 @@ public class NashornJsEngineTest { @Test public void metadataAccessibleForFilter() throws ScriptException { String function = "return metadata.humidity < 15;"; - scriptEngine = new NashornJsEngine(function, "Filter"); + scriptEngine = new JsScriptEngine(jsSandboxService, function, "Filter"); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); metaData.putValue("humidity", "99"); @@ -96,7 +111,7 @@ public class NashornJsEngineTest { @Test public void dataAccessibleForFilter() throws ScriptException { String function = "return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 7 && msg.bigObj.prop == 42;"; - scriptEngine = new NashornJsEngine(function, "Filter"); + scriptEngine = new JsScriptEngine(jsSandboxService, function, "Filter"); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "7"); metaData.putValue("humidity", "99"); @@ -116,7 +131,7 @@ public class NashornJsEngineTest { "};\n" + "\n" + "return nextRelation(metadata, msg);"; - scriptEngine = new NashornJsEngine(jsCode, "Switch"); + scriptEngine = new JsScriptEngine(jsSandboxService, jsCode, "Switch"); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "10"); metaData.putValue("humidity", "99"); @@ -137,7 +152,7 @@ public class NashornJsEngineTest { "};\n" + "\n" + "return nextRelation(metadata, msg);"; - scriptEngine = new NashornJsEngine(jsCode, "Switch"); + scriptEngine = new JsScriptEngine(jsSandboxService, jsCode, "Switch"); TbMsgMetaData metaData = new TbMsgMetaData(); metaData.putValue("temp", "10"); metaData.putValue("humidity", "99"); diff --git a/application/src/test/java/org/thingsboard/server/service/script/TestNashornJsSandboxService.java b/application/src/test/java/org/thingsboard/server/service/script/TestNashornJsSandboxService.java new file mode 100644 index 0000000000..84cca0cfe1 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/script/TestNashornJsSandboxService.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2018 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.thingsboard.server.service.script; + +import delight.nashornsandbox.NashornSandbox; +import delight.nashornsandbox.NashornSandboxes; + +import javax.script.ScriptException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class TestNashornJsSandboxService implements JsSandboxService { + + private NashornSandbox sandbox = NashornSandboxes.create(); + private ExecutorService monitorExecutorService; + + public TestNashornJsSandboxService(int monitorThreadPoolSize, long maxCpuTime) { + monitorExecutorService = Executors.newFixedThreadPool(monitorThreadPoolSize); + sandbox.setExecutor(monitorExecutorService); + sandbox.setMaxCPUTime(maxCpuTime); + sandbox.allowNoBraces(false); + sandbox.setMaxPreparedStatements(30); + } + + @Override + public Object eval(String js) throws ScriptException { + return sandbox.eval(js); + } + + @Override + public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException { + return sandbox.getSandboxedInvocable().invokeFunction(name, args); + } + + public void destroy() { + if (monitorExecutorService != null) { + monitorExecutorService.shutdownNow(); + } + } +} diff --git a/pom.xml b/pom.xml index 97a02714a4..47d4413ed2 100755 --- a/pom.xml +++ b/pom.xml @@ -81,6 +81,7 @@ org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* 5.0.2 + 0.1.14 @@ -814,6 +815,11 @@ rest ${elasticsearch.version} + + org.javadelight + delight-nashorn-sandbox + ${delight-nashorn-sandbox.version} +