3

For a server program written in Java, I need to add an interpreter for a (to-be specified) query language. Users should be able to send self-written query "programs" to this server, and receive results back (basically just a list of strings). The language for the queries is not specified yet, so I thought of using clojure as scripting language here - so users send mini-programs to the server, which evaluates them and, if the results have the correct type, sends them back.

I could get it to work, by using "RT.readString", accessing the "eval" function by querying ```Var EVAL=RT.var("clojure.core","eval")ยดยดยด and using EVAL to evaluate the result returned by RT.readString before.

To make this somewhat better usable, I need to enable some Java imports, which should always be enabled. Logically, these imports should only be run once - how can I achieve that? I could not get it to work - when I tried to run the specification of the imports first, the query string using these imports did not work.

Is it possible, to achieve this goal - having a one-time initialization program fragment run first, and have following scripts use this? I scoured the web, but the examples of "call Clojure from Java" I found all had a different bend - they were centered on executing specific Clojure programs from Java, and not geared to allow executing arbitrary programs in Clojure.

Furthermore, I looked at how I could set Clojure variables to specific Java objects - I still have no idea on how to achieve this. Basically I want to be able to put certain Java objects "into" the Clojure interpreter, and let the following code use this (ideally this would be a thread-local var - Clojure supports that, AFAIK). But how?

Is this (using Clojure to "script" another Java program) even possible? And is it possible to restrict the code which may be called? I do not want to start using custom ClassLoader classes, and SecurityManager instances, but it seems if I want to block certain calls, this is the only option I have. Is this correct?

Wolfgang Liebich
  • 400
  • 2
  • 12

1 Answers1

3

This is a complex problem where Clojure can really shine. Let's summarise all though points. You want:

  1. to evaluate some Clojure code coming from user input,
  2. to make sure it is secured,
  3. this code needs to run in a context you provide,
  4. you need to capture all outputs.

1. Evaluating Clojure code from the Java world

As you saw, using RT from the Java world for this use case quickly shows limitations. It's simpler to create a Clojure namespace like this one:

(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)))

And to call it from java with this minimal example:

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.
    }
}

You can now evaluate any Clojure code from the Java world.

2. Make sure this code is secured

Securing the read phase

As explained in this thread, the safe way to read Clojure code from non-trusted sources is to avoid clojure.core/read-string and instead, use clojure.edn/read-string which is designed for this purpose.

Securing the code

You obviously don't want your end-users to be able to access the whole JVM world from your interpreter. Instead, you'd like them to be able to use a set of pre-defined operations you can control.

Since Clojure code is just data, you can walk parsed code and validate it against a spec/schema or more simply using a function:

(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
      ))

It's up to you to carefully pick safe operations and literals.

3. Providing context

When parsed from a string, non-namespaced symbols will refer to the current namespace. You can then provide functions, values or any other binding in the interpreter namespace, or provide an alias.

(ns interpreter.tools)

(defn cos [x]
  (java.lang.Math/cos x))

(defn version []
  "Interpreter v0.1")

Adapt interperter.core accordingly:

(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})

Now tools/cos and tools/version functions are available in your interpreter.

4. Capturing all outputs

It would be best to only provide pure functions as available operations, but we don't always control the real world, especially what happens in dependencies. To make sure you capture STDOUT, you can rewrite eval-script this way:

(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)})))

Let's try it from Clojure:

(eval-script "(do (tools/print-version) (tools/cos 1)))")
;; => {:result 0.5403023058681398, :out "Interpreter v0.1\n"}

Let's try it from Java:

java -cp `lein cp` interpreter.Runner "(do (tools/print-version) (tools/cos 1)))"

=> java.lang.RuntimeException: No such namespace: tools

Here is how to fix it:

  1. Make a function load-tools! in interpreter.core
(defn load-tools! []
  (require '[interpreter.tools :as tools]))
  1. Call it once from Java before evaluating your script:
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]));

Let's try it again:

java -cp `lein cp` interpreter.Runner "(do (tools/print-version) (tools/cos 1))"

=> {:result 0.5403023058681398, :out "Interpreter v0.1\n"}

Here is the full code: https://github.com/ggeoffrey/interpreter-demo