这是一个复杂的问题,Clojure 可以真正发挥作用。让我们总结一下所有要点。你想要:
- 评估来自用户输入的一些 Clojure 代码,
- 以确保其安全,
- 此代码需要在您提供的上下文中运行,
- 您需要捕获所有输出。
1。从 Java 世界评估 Clojure 代码
如您所见,在此用例中使用 Java 世界中的 RT 很快就会显示出局限性。
创建一个像这样的 Clojure 命名空间更简单:
(ns interpreter.core)
;; This is unsafe and dangerous, we are going to make it safer in the next steps.
(defn unsafe-eval [code]
(eval (read-string code)))
并用这个最小的例子从 java 中调用它:
package interpreter;
import clojure.java.api.Clojure;
import clojure.lang.IFn;
class Runner{
public static void main(String[] args){
// Load the `require` function
IFn require = Clojure.var("clojure.core", "require");
// require the clojure namespace
require.invoke(Clojure.read("interpreter.core"));
// load the `unsafe-eval` function we crafted above
IFn unsafe_eval = Clojure.var("interpreter.core", "unsafe-eval");
// execute it
System.out.println("Result: " +unsafe_eval.invoke("(+ 1 2)"));
// => `Result: 3` get printed.
}
}
您现在可以评估来自 Java 世界的任何 Clojure 代码。
2。确保此代码是安全的
保护读取阶段
正如this thread 中所述,从不可信来源读取 Clojure 代码的安全方法是避免使用 clojure.core/read-string,而是使用专为此目的设计的 clojure.edn/read-string。
保护代码
您显然不希望您的最终用户能够从您的解释器访问整个 JVM 世界。相反,您希望他们能够使用一组您可以控制的预定义操作。
由于 Clojure 代码只是数据,因此您可以遍历已解析的代码并根据规范/模式或更简单地使用函数对其进行验证:
(ns interpreter.core
(:require [clojure.edn :as edn]
[clojure.walk :as walk]))
;; Users are only allowed to perform operations listed in this set.
(def allowed-operations '#{+ -})
;; Users are also allowed to use lists and numbers
(defn allowed? [x]
(or (list? x) ;; Clojure code is mostly made of lists
(number? x)
(contains? allowed-operations x)))
(defn validate! [parsed-code]
(walk/postwalk (fn [x] (if (allowed? x)
x
(throw (ex-info "Unknown identifier" {:value x}))))
parsed-code))
;; This is safe as long as `allowed-operations` do not list anything sensitive
(defn eval-script [code]
(-> (edn/read-string code) ;; read safely
(validate!) ;; stop on forbidden operations or literals
(eval) ;; run
))
您可以谨慎选择安全操作和文字。
3。提供上下文
从字符串解析时,非命名空间符号将引用当前命名空间。然后,您可以在解释器命名空间中提供函数、值或任何其他绑定,或者提供别名。
(ns interpreter.tools)
(defn cos [x]
(java.lang.Math/cos x))
(defn version []
"Interpreter v0.1")
相应地调整interperter.core:
(ns interpreter.core
(:require [clojure.edn :as edn]
[clojure.walk :as walk]
[interpreter.tools :as tools]));; import your custom operations
;; Add them to the allowed operations set
(def allowed-operations '#{+ - tools/cos tools/version})
现在您的解释器中可以使用 tools/cos 和 tools/version 函数。
4。捕获所有输出
最好只提供纯函数作为可用操作,但我们并不总是控制现实世界,尤其是依赖项中发生的事情。
为确保捕获 STDOUT,您可以这样重写 eval-script:
(ns interperter.tools)
(defn print-version[]
(println (version)))
(ns interpreter.core
(:require [clojure.edn :as edn]
[clojure.walk :as walk]
[interpreter.tools :as tools])
(:import java.io.StringWriter))
(def allowed-operations '#{+ - do tools/cos tools/version tools/print-version})
(defn eval-script [code]
(let [out (new java.io.StringWriter)]
(binding [*out* out] ;; redirect System.out to the `out` StringWriter
{:result (-> (edn/read-string code) ;; read safely
(validate!) ;; stop on forbidden operations or literals
(eval) ;; run
)
:out (str out)})))
让我们从 Clojure 中尝试一下:
(eval-script "(do (tools/print-version) (tools/cos 1)))")
;; => {:result 0.5403023058681398, :out "Interpreter v0.1\n"}
让我们从 Java 中尝试一下:
java -cp `lein cp` interpreter.Runner "(do (tools/print-version) (tools/cos 1)))"
=> java.lang.RuntimeException: No such namespace: tools
解决方法如下:
- 在
interpreter.core 中创建一个函数load-tools!
(defn load-tools! []
(require '[interpreter.tools :as tools]))
- 在评估脚本之前从 Java 调用一次:
IFn load_tools = Clojure.var("interpreter.core", "load-tools!");
load_tools.invoke();
IFn eval_script = Clojure.var("interpreter.core", "eval-script");
System.out.println("Result: " +eval_script.invoke(args[0]));
让我们再试一次:
java -cp `lein cp` interpreter.Runner "(do (tools/print-version) (tools/cos 1))"
=> {:result 0.5403023058681398, :out "Interpreter v0.1\n"}
这里是完整代码:https://github.com/ggeoffrey/interpreter-demo