21 changed files with 695 additions and 302 deletions
@ -1,65 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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.script.api; |
|||
|
|||
import lombok.Getter; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.server.common.stats.TbApiUsageReportClient; |
|||
import org.thingsboard.server.common.stats.TbApiUsageStateClient; |
|||
|
|||
import java.util.Optional; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@Slf4j |
|||
@ConditionalOnProperty(prefix = "js", value = "evaluator", havingValue = "local", matchIfMissing = true) |
|||
@Service |
|||
public class NashornJsInvokeService extends AbstractNashornJsInvokeService { |
|||
|
|||
@Value("${js.local.use_js_sandbox}") |
|||
private boolean useJsSandbox; |
|||
|
|||
@Getter |
|||
@Value("${js.local.monitor_thread_pool_size}") |
|||
private int monitorThreadPoolSize; |
|||
|
|||
@Getter |
|||
@Value("${js.local.max_cpu_time}") |
|||
private long maxCpuTime; |
|||
|
|||
@Getter |
|||
@Value("${js.local.max_errors}") |
|||
private int maxErrors; |
|||
|
|||
@Value("${js.local.max_black_list_duration_sec:60}") |
|||
private int maxBlackListDurationSec; |
|||
|
|||
public NashornJsInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) { |
|||
super(apiUsageStateClient, apiUsageReportClient); |
|||
} |
|||
|
|||
@Override |
|||
protected boolean useJsSandbox() { |
|||
return useJsSandbox; |
|||
} |
|||
|
|||
@Override |
|||
protected long getMaxBlacklistDuration() { |
|||
return TimeUnit.SECONDS.toMillis(maxBlackListDurationSec); |
|||
} |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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.script.api.js; |
|||
|
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import lombok.Getter; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.thingsboard.script.api.AbstractScriptInvokeService; |
|||
import org.thingsboard.script.api.RuleNodeScriptFactory; |
|||
import org.thingsboard.script.api.ScriptType; |
|||
import org.thingsboard.server.common.stats.TbApiUsageReportClient; |
|||
import org.thingsboard.server.common.stats.TbApiUsageStateClient; |
|||
|
|||
import java.util.Map; |
|||
import java.util.Optional; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
|
|||
/** |
|||
* Created by ashvayka on 26.09.18. |
|||
*/ |
|||
@Slf4j |
|||
public abstract class AbstractJsInvokeService extends AbstractScriptInvokeService { |
|||
|
|||
protected Map<UUID, String> scriptIdToNameMap = new ConcurrentHashMap<>(); |
|||
|
|||
@Getter |
|||
@Value("${js.max_total_args_size:100000}") |
|||
private long maxTotalArgsSize; |
|||
@Getter |
|||
@Value("${js.max_result_size:300000}") |
|||
private long maxResultSize; |
|||
@Getter |
|||
@Value("${js.max_script_body_size:50000}") |
|||
private long maxScriptBodySize; |
|||
|
|||
protected AbstractJsInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) { |
|||
super(apiUsageStateClient, apiUsageReportClient); |
|||
} |
|||
|
|||
@Override |
|||
protected boolean isScriptPresent(UUID scriptId) { |
|||
return scriptIdToNameMap.containsKey(scriptId); |
|||
} |
|||
|
|||
@Override |
|||
public ListenableFuture<Void> release(UUID scriptId) { |
|||
String functionName = scriptIdToNameMap.get(scriptId); |
|||
if (functionName != null) { |
|||
try { |
|||
scriptIdToNameMap.remove(scriptId); |
|||
disabledScripts.remove(scriptId); |
|||
doRelease(scriptId, functionName); |
|||
} catch (Exception e) { |
|||
return Futures.immediateFailedFuture(e); |
|||
} |
|||
} |
|||
return Futures.immediateFuture(null); |
|||
} |
|||
|
|||
@Override |
|||
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, Object[] args) { |
|||
return doInvokeFunction(scriptId, scriptIdToNameMap.get(scriptId), args); |
|||
} |
|||
|
|||
@Override |
|||
protected ListenableFuture<UUID> doEvalScript(ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames) { |
|||
String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_'); |
|||
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames); |
|||
return doEval(scriptId, functionName, jsScript); |
|||
} |
|||
|
|||
@Override |
|||
protected void doRelease(UUID scriptId) throws Exception { |
|||
String functionName = scriptIdToNameMap.remove(scriptId); |
|||
doRelease(scriptId, functionName); |
|||
} |
|||
|
|||
protected abstract ListenableFuture<UUID> doEval(UUID scriptId, String functionName, String scriptBody); |
|||
|
|||
protected abstract ListenableFuture<Object> doInvokeFunction(UUID scriptId, String functionName, Object[] args); |
|||
|
|||
protected abstract void doRelease(UUID scriptId, String functionName) throws Exception; |
|||
|
|||
private String generateJsScript(ScriptType scriptType, String functionName, String scriptBody, String... argNames) { |
|||
if (scriptType == ScriptType.RULE_NODE_SCRIPT) { |
|||
return RuleNodeScriptFactory.generateRuleNodeScript(functionName, scriptBody, argNames); |
|||
} |
|||
throw new RuntimeException("No script factory implemented for scriptType: " + scriptType); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,152 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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.script.api.mvel; |
|||
|
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import com.google.common.util.concurrent.ListeningExecutorService; |
|||
import com.google.common.util.concurrent.MoreExecutors; |
|||
import lombok.Getter; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.mvel2.MVEL; |
|||
import org.mvel2.ParserContext; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.scheduling.annotation.Scheduled; |
|||
import org.thingsboard.common.util.ThingsBoardExecutors; |
|||
import org.thingsboard.script.api.AbstractScriptInvokeService; |
|||
import org.thingsboard.script.api.ScriptType; |
|||
import org.thingsboard.server.common.stats.TbApiUsageReportClient; |
|||
import org.thingsboard.server.common.stats.TbApiUsageStateClient; |
|||
|
|||
import javax.annotation.PostConstruct; |
|||
import javax.annotation.PreDestroy; |
|||
import java.io.Serializable; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
import java.util.Optional; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ExecutionException; |
|||
import java.util.concurrent.Executor; |
|||
|
|||
@Slf4j |
|||
public class MvelInvokeService extends AbstractScriptInvokeService { |
|||
|
|||
protected Map<UUID, MvelScript> scriptMap = new ConcurrentHashMap<>(); |
|||
private ParserContext parserContext; |
|||
|
|||
@Getter |
|||
@Value("${mvel.max_total_args_size:100000}") |
|||
private long maxTotalArgsSize; |
|||
@Getter |
|||
@Value("${mvel.max_result_size:300000}") |
|||
private long maxResultSize; |
|||
@Getter |
|||
@Value("${mvel.max_script_body_size:50000}") |
|||
private long maxScriptBodySize; |
|||
|
|||
@Getter |
|||
@Value("${mvel.max_errors:3}") |
|||
private int maxErrors; |
|||
|
|||
@Getter |
|||
@Value("${mvel.max_black_list_duration_sec:60}") |
|||
private int maxBlackListDurationSec; |
|||
|
|||
@Getter |
|||
@Value("${mvel.max_requests_timeout:0}") |
|||
private long maxInvokeRequestsTimeout; |
|||
|
|||
@Getter |
|||
@Value("${mvel.stats.enabled:false}") |
|||
private boolean statsEnabled; |
|||
|
|||
private ListeningExecutorService executor; |
|||
|
|||
protected MvelInvokeService(Optional<TbApiUsageStateClient> apiUsageStateClient, Optional<TbApiUsageReportClient> apiUsageReportClient) { |
|||
super(apiUsageStateClient, apiUsageReportClient); |
|||
} |
|||
|
|||
@Scheduled(fixedDelayString = "${mvel.stats.print_interval_ms:10000}") |
|||
public void printStats() { |
|||
super.printStats(); |
|||
} |
|||
|
|||
@PostConstruct |
|||
public void init() { |
|||
super.init(); |
|||
parserContext = new ParserContext(new TbMvelParserConfiguration()); |
|||
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(2, "mvel-executor")); |
|||
} |
|||
|
|||
@PreDestroy |
|||
public void destroy() { |
|||
if (executor != null) { |
|||
executor.shutdownNow(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
protected String getStatsName() { |
|||
return "MVEL Scripts Stats"; |
|||
} |
|||
|
|||
@Override |
|||
protected Executor getCallbackExecutor() { |
|||
return MoreExecutors.directExecutor(); |
|||
} |
|||
|
|||
@Override |
|||
protected boolean isScriptPresent(UUID scriptId) { |
|||
return scriptMap.containsKey(scriptId); |
|||
} |
|||
|
|||
@Override |
|||
protected ListenableFuture<UUID> doEvalScript(ScriptType scriptType, String scriptBody, UUID scriptId, String[] argNames) { |
|||
//TODO: executor, check expression for "new" and ?
|
|||
return executor.submit(() -> { |
|||
try { |
|||
Serializable compiledScript = MVEL.compileExpression(scriptBody, parserContext); |
|||
MvelScript script = new MvelScript(compiledScript, argNames); |
|||
scriptMap.put(scriptId, script); |
|||
return scriptId; |
|||
} catch (Exception e) { |
|||
log.debug("Failed to compile MVEL script: {}", scriptBody, e); |
|||
throw new ExecutionException(e); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, Object[] args) { |
|||
return executor.submit(() -> { |
|||
MvelScript script = scriptMap.get(scriptId); |
|||
if (script == null) { |
|||
throw new RuntimeException("Script not found!"); |
|||
} |
|||
try { |
|||
return MVEL.executeExpression(script.getCompiledScript(), script.createVars(args)); |
|||
} catch (OutOfMemoryError e) { |
|||
Runtime.getRuntime().gc(); |
|||
throw new RuntimeException("Memory error!"); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
@Override |
|||
protected void doRelease(UUID scriptId) throws Exception { |
|||
scriptMap.remove(scriptId); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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.script.api.mvel; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.io.Serializable; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
@Data |
|||
public class MvelScript { |
|||
|
|||
private final Serializable compiledScript; |
|||
private final String[] argNames; |
|||
|
|||
public Map createVars(Object[] args) { |
|||
if (args == null || args.length != argNames.length) { |
|||
throw new IllegalArgumentException("Invalid number of argument values"); |
|||
} |
|||
var result = new HashMap<>(); |
|||
for (int i = 0; i < argNames.length; i++) { |
|||
result.put(argNames[i], args[i]); |
|||
} |
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,90 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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.script.api.mvel; |
|||
|
|||
import org.mvel2.compiler.AbstractParser; |
|||
|
|||
import java.net.URL; |
|||
import java.net.URLClassLoader; |
|||
import java.util.HashSet; |
|||
import java.util.Set; |
|||
|
|||
public class TbMvelClassLoader extends URLClassLoader { |
|||
|
|||
private static final Set<String> allowedClasses = new HashSet<>(); |
|||
private static final Set<String> allowedPackages = new HashSet<>(); |
|||
|
|||
static { |
|||
|
|||
AbstractParser.LITERALS.remove("System"); |
|||
AbstractParser.LITERALS.remove("Runtime"); |
|||
AbstractParser.LITERALS.remove("Class"); |
|||
AbstractParser.LITERALS.remove("ClassLoader"); |
|||
AbstractParser.LITERALS.remove("Thread"); |
|||
AbstractParser.LITERALS.remove("Compiler"); |
|||
AbstractParser.LITERALS.remove("ThreadLocal"); |
|||
AbstractParser.LITERALS.remove("SecurityManager"); |
|||
|
|||
AbstractParser.CLASS_LITERALS.remove("System"); |
|||
AbstractParser.CLASS_LITERALS.remove("Runtime"); |
|||
AbstractParser.CLASS_LITERALS.remove("Class"); |
|||
AbstractParser.CLASS_LITERALS.remove("ClassLoader"); |
|||
AbstractParser.CLASS_LITERALS.remove("Thread"); |
|||
AbstractParser.CLASS_LITERALS.remove("Compiler"); |
|||
AbstractParser.CLASS_LITERALS.remove("ThreadLocal"); |
|||
AbstractParser.CLASS_LITERALS.remove("SecurityManager"); |
|||
|
|||
AbstractParser.CLASS_LITERALS.values().forEach(val -> allowedClasses.add(((Class) val).getName())); |
|||
} |
|||
|
|||
static { |
|||
allowedPackages.add("org.mvel2"); |
|||
allowedPackages.add("java.util"); |
|||
} |
|||
|
|||
public TbMvelClassLoader() { |
|||
super(new URL[0], Thread.currentThread().getContextClassLoader()); |
|||
} |
|||
|
|||
@Override |
|||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { |
|||
if (!classNameAllowed(name)) { |
|||
throw new ClassNotFoundException(); |
|||
} |
|||
return super.loadClass(name, resolve); |
|||
} |
|||
|
|||
@Override |
|||
public Class<?> loadClass(String name) throws ClassNotFoundException { |
|||
if (!classNameAllowed(name)) { |
|||
throw new ClassNotFoundException(); |
|||
} |
|||
return super.loadClass(name); |
|||
} |
|||
|
|||
private boolean classNameAllowed(String name) { |
|||
if (allowedClasses.contains(name)) { |
|||
return true; |
|||
} |
|||
for (String pkgName : allowedPackages) { |
|||
if (name.startsWith(pkgName)) { |
|||
return true; |
|||
} |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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.script.api.mvel; |
|||
|
|||
import org.mvel2.ParserConfiguration; |
|||
import org.mvel2.integration.VariableResolverFactory; |
|||
|
|||
public class TbMvelParserConfiguration extends ParserConfiguration { |
|||
|
|||
private static final long serialVersionUID = 5558151976348875590L; |
|||
|
|||
TbMvelParserConfiguration() { |
|||
setClassLoader(new TbMvelClassLoader()); |
|||
} |
|||
|
|||
@Override |
|||
public VariableResolverFactory getVariableFactory(VariableResolverFactory factory) { |
|||
if (Thread.interrupted()) { |
|||
throw new RuntimeException("Thread is interrupted!"); |
|||
} |
|||
return new TbMvelResolverFactory(factory); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
/** |
|||
* Copyright © 2016-2022 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.script.api.mvel; |
|||
|
|||
import org.mvel2.integration.VariableResolver; |
|||
import org.mvel2.integration.VariableResolverFactory; |
|||
import org.mvel2.integration.impl.StackResetResolverFactory; |
|||
|
|||
public class TbMvelResolverFactory extends StackResetResolverFactory { |
|||
|
|||
public TbMvelResolverFactory(VariableResolverFactory delegate) { |
|||
super(delegate); |
|||
} |
|||
|
|||
@Override |
|||
public VariableResolver getVariableResolver(String name) { |
|||
if (Thread.interrupted()) { |
|||
throw new RuntimeException("Thread is interrupted!"); |
|||
} |
|||
return super.getVariableResolver(name); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue