Это сложная проблема, когда Clojure действительно может сиять. Давайте подведем итоги по всем пунктам. Вы хотите:
- для оценки некоторого кода Clojure, поступающего из пользовательского ввода,
- , чтобы убедиться, что он защищен,
- этот код должен выполняться в контексте, который вы обеспечьте,
- вам нужно захватить все выходы.
1. Оценка кода Clojure из мира Java
Как вы видели, использование RT
из мира Java для этого варианта использования быстро показывает ограничения. Проще создать пространство имен 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.
}
}
Теперь вы можете оценить любое Код Clojure из мира Java.
2. Убедитесь, что этот код защищен
Защита фазы чтения
Как объяснено в этой теме , безопасный способ чтения кода 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
Вот как это исправить:
- Сделать функцию
load-tools!
in interpreter.core
(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