Как я могу использовать clojure в качестве языка сценариев для программы Java? - PullRequest
1 голос
/ 20 февраля 2020

Для серверной программы, написанной на Java, мне нужно добавить интерпретатор для (должен быть указан) языка запросов. Пользователи должны иметь возможность отправлять «программы», которые пишут самостоятельно, на этот сервер и получать результаты обратно (в основном просто список строк). Язык для запросов еще не указан, поэтому я подумал об использовании clojure в качестве языка сценариев - чтобы пользователи отправляли мини-программы на сервер, который оценивает их и, если результаты имеют правильный тип, отправляет их обратно.

Я мог бы заставить его работать, используя "RT.readString", получая доступ к функции "eval", запрашивая `` `Var EVAL = RT.var (" clojure.core "," eval ") ´´´ и использование EVAL для оценки результата, возвращаемого RT.readString ранее.

Чтобы сделать это несколько более удобным для использования, мне нужно включить некоторые операции импорта Java, которые всегда должны быть включены. По логике вещей, этот импорт должен выполняться только один раз - как мне этого добиться? Я не мог заставить его работать - когда я пытался сначала запустить спецификацию импорта, строка запроса, использующая эти импорты, не работала.

Возможно ли достичь этой цели - иметь одноразовый Фрагмент программы инициализации запускается первым, и следующие сценарии используют это? Я искал в Интернете, но примеры «вызова Clojure из Java», которые я обнаружил, имели различный изгиб - они были сосредоточены на выполнении определенных c программ Clojure из Java и не были приспособлены для разрешения выполнения произвольных программ в Clojure.

Кроме того, я посмотрел, как можно установить переменные Clojure для указания c Java объектов - я до сих пор не знаю, как этого добиться. По сути, я хочу иметь возможность помещать определенные Java объекты «в» интерпретатор Clojure, и позволить следующему коду использовать это (в идеале это будет локальная переменная потока - Clojure поддерживает это, AFAIK). Но как?

Возможно ли это (используя Clojure для «написания сценария» другой Java программы)? И возможно ли ограничить код, который может быть вызван? Я не хочу начинать использовать пользовательские классы ClassLoader и экземпляры SecurityManager, но, похоже, если я хочу заблокировать определенные вызовы, это единственная опция, которая у меня есть. Это правильно?

1 Ответ

1 голос
/ 06 марта 2020

Это сложная проблема, когда Clojure действительно может сиять. Давайте подведем итоги по всем пунктам. Вы хотите:

  1. для оценки некоторого кода Clojure, поступающего из пользовательского ввода,
  2. , чтобы убедиться, что он защищен,
  3. этот код должен выполняться в контексте, который вы обеспечьте,
  4. вам нужно захватить все выходы.

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

Вот как это исправить:

  1. Сделать функцию 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

...