├── .formatter.exs ├── .gitignore ├── LICENSE ├── README.md ├── bunny.lua ├── lib └── sandbox.ex ├── mix.exs ├── mix.lock └── test ├── lua ├── animal.lua └── mobility.lua ├── sandbox_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build/ 2 | /cover/ 3 | /deps/ 4 | /doc/ 5 | /.fetch 6 | erl_crash.dump 7 | *.ez 8 | sandbox-*.tar 9 | .idea 10 | .elixir_ls 11 | .DS_Store 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sandbox 2 | 3 | Sandbox helps to provide restricted, isolated scripting environments for Elixir through the use of embedded Lua. 4 | Powered by Robert Virding's amazing Luerl library, its minimal API is focused on facilitating the creation of "safe" server-side DSLs. 5 | 6 | The API has been modified from the Erlang original such that functions can modify the state of the VM (mutations) or return a discrete value, but not both. 7 | The `:luerl_sandbox` module is utilized wherever possible. 8 | 9 | ## Installation 10 | 11 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 12 | by adding `sandbox` to your list of dependencies in `mix.exs`: 13 | 14 | ```elixir 15 | def deps do 16 | [ 17 | {:sandbox, "~> 0.1.0"} 18 | ] 19 | end 20 | ``` 21 | 22 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 23 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 24 | be found at [https://hexdocs.pm/sandbox](https://hexdocs.pm/sandbox). 25 | 26 | -------------------------------------------------------------------------------- /bunny.lua: -------------------------------------------------------------------------------- 1 | 2 | animal_voices = {bunny="silence", cow="moo", cat="meow", dog="woof"} 3 | 4 | function speak(animal) 5 | local v = animal_voices[animal] 6 | return v 7 | end -------------------------------------------------------------------------------- /lib/sandbox.ex: -------------------------------------------------------------------------------- 1 | defmodule Sandbox do 2 | @moduledoc """ 3 | 4 | #### --- Warning --- 5 | #### This library is under heavy development and will have breaking changes until the 1.0.0 release. 6 | 7 | Sandbox provides restricted, isolated scripting environments for Elixir through the use of embedded Lua. 8 | 9 | This project is powered by Robert Virding's amazing [Luerl](https://github.com/rvirding/luerl), an Erlang library that lets one execute Lua scripts on the BEAM. 10 | 11 | Luerl executes Lua code _without_ running a Lua VM as a separate application! Rather, the state of the VM is used as a 12 | data structure that can be externally manipulated and processed. 13 | 14 | The `:luerl_sandbox` module is utilized wherever possible. This limits access to dangerous core libraries. 15 | It also permits Lua scripts to be run with enforced CPU reduction limits. To work with Lua's full library, use 16 | `Sandbox.unsafe_init/0` as opposed to `Sandbox.init/0`. 17 | 18 | Conventions followed in this library: 19 | 20 | - Functions beginning with `eval` return a result from Lua. 21 | - Functions starting with `play` return a new Lua state. 22 | - Functions preceded by `run` return a tuple of `{result, new_state}` 23 | - All functions return ok-error tuples such as `{:ok, value}` or `{:error, reason}` unless followed by a bang. 24 | - Elixir functions exposed to Lua take two arguments: a Lua state and a list of Lua arguments. They 25 | should return a value corresponding to the `eval`, `play` or `run` responses. 26 | - The `max_reductions` argument defaults to `0`, corresponding to unlimited reductions. 27 | 28 | """ 29 | @unlimited_reductions 0 30 | @sandbox_error "Lua Sandbox Error: " 31 | @reduction_error @sandbox_error <> "exceeded reduction limit!" 32 | 33 | @typedoc """ 34 | Compiled Lua code that can be transferred between Lua states. 35 | """ 36 | @type lua_chunk :: {:lua_func, any(), any(), any(), any(), any()} 37 | @typedoc """ 38 | The representation of an entire Lua virtual machine and its current state. 39 | """ 40 | @type lua_state :: 41 | {:luerl, any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), 42 | any(), any(), any()} 43 | @typedoc """ 44 | Lua code as either a raw string or compile chunk. 45 | """ 46 | @type lua_code :: lua_chunk() | String.t() 47 | @typedoc """ 48 | Lua values represented as Elixir data structures. 49 | """ 50 | @type lua_value :: number() | String.t() | [tuple()] | nil 51 | @typedoc """ 52 | A dot-delimited name or list of names representing a table path in Lua such as `math.floor` or `["math", "floor"]`. 53 | """ 54 | @type lua_path :: String.t() | [String.t()] 55 | @typedoc """ 56 | An Elixir function that can be invoked through Lua. It takes a Lua state and a list of Lua arguments and returns 57 | a tuple containing a result and a new Lua state. 58 | """ 59 | @type elixir_run_fun :: (lua_state(), [lua_value()] -> {any(), lua_state()}) 60 | @typedoc """ 61 | An Elixir function that can be invoked through Lua. It takes a Lua state and a list of Lua arguments and returns 62 | a result. The Lua state acts as a context but is not modified. 63 | """ 64 | @type elixir_eval_fun :: (lua_state(), [lua_value()] -> any()) 65 | @typedoc """ 66 | An Elixir function that can be invoked through Lua. It takes a Lua state and a list of Lua arguments and returns 67 | a new Lua state. The result of this function is not exposed to Lua. 68 | """ 69 | @type elixir_play_fun :: (lua_state(), [lua_value()] -> lua_state()) 70 | 71 | @doc """ 72 | Creates a Lua state with "dangerous" core library features such as file IO and networking removed. 73 | """ 74 | def init() do 75 | :luerl_sandbox.init() 76 | end 77 | 78 | @doc """ 79 | Creates a Lua state with access to Lua's standard library. The `max_reductions` feature of `Sandbox` is still 80 | available, but "dangerous" core library features such as file IO and networking are still available. 81 | """ 82 | def unsafe_init() do 83 | :luerl.init() 84 | end 85 | 86 | @doc ~S""" 87 | Evaluates a Lua string or chunk against the given Lua state and returns the result in an ok-error tuple. The state itself is not modified. 88 | 89 | ## Examples 90 | 91 | iex> Sandbox.init() |> Sandbox.eval("return 3 + 4") 92 | {:ok, 7.0} 93 | 94 | iex> Sandbox.init() |> Sandbox.eval("return math.floor(9.9)") 95 | {:ok, 9.0} 96 | 97 | """ 98 | @spec eval(lua_state(), lua_code(), non_neg_integer()) :: {:ok, lua_value()} | {:error, any()} 99 | def eval(state, code, max_reductions \\ @unlimited_reductions) do 100 | case :luerl_sandbox.run(code, state, max_reductions) do 101 | {:error, e} -> {:error, e} 102 | {[{:tref, _} = table | _], new_state} -> {:ok, :luerl.decode(table, new_state)} 103 | {[result | _], _new_state} -> {:ok, result} 104 | {[], _} -> {:ok, nil} 105 | end 106 | end 107 | 108 | @doc ~S""" 109 | Same as `eval/3`, but will return the raw result or raise a `RuntimeError`. 110 | 111 | ## Examples 112 | 113 | iex> Sandbox.init() |> Sandbox.eval!("return 3 + 4") 114 | 7.0 115 | 116 | iex> Sandbox.init() |> Sandbox.eval!("return math.floor(9.9)") 117 | 9.0 118 | 119 | """ 120 | @spec eval!(lua_state(), lua_code(), non_neg_integer()) :: lua_value() 121 | def eval!(state, code, max_reductions \\ @unlimited_reductions) do 122 | case :luerl_sandbox.run(code, state, max_reductions) do 123 | {:error, {:reductions, _n}} -> raise(@reduction_error) 124 | {:error, reason} -> raise(@sandbox_error <> "#{inspect(reason)}") 125 | {[{:tref, _} = table | _], new_state} -> :luerl.decode(table, new_state) 126 | {[result | _], _new_state} -> result 127 | {[], _new_state} -> nil 128 | end 129 | end 130 | 131 | @doc """ 132 | Evaluates a Lua file against the given Lua state and returns the result in an ok-error tuple. The state itself is not modified. 133 | """ 134 | 135 | @spec eval_file(lua_state(), String.t(), non_neg_integer()) :: 136 | {:ok, lua_value()} | {:error, any()} 137 | def eval_file(state, file_path, max_reductions \\ @unlimited_reductions) do 138 | with {:ok, code} <- File.read(file_path), 139 | {:ok, result} <- eval(state, code, max_reductions) do 140 | {:ok, result} 141 | else 142 | {:error, reason} -> {:error, reason} 143 | end 144 | end 145 | 146 | @doc """ 147 | Same as `eval_file/3`, but will return the raw result or raise a `RuntimeError`. 148 | """ 149 | 150 | @spec eval_file!(lua_state(), String.t(), non_neg_integer()) :: lua_value() 151 | def eval_file!(state, file_path, max_reductions \\ @unlimited_reductions) do 152 | code = File.read!(file_path) 153 | eval!(state, code, max_reductions) 154 | end 155 | 156 | @doc """ 157 | Calls a function defined in the the Lua state and returns only the result. The state itself is not modified. 158 | Lua functions in the Lua state can be referenced by their `lua_path`, being a string or list such as `math.floor` or `["math", "floor"]`. 159 | """ 160 | 161 | @spec eval_function!(lua_state(), lua_path(), non_neg_integer()) :: lua_value() 162 | def eval_function!(state, path, args \\ [], max_reductions \\ @unlimited_reductions) 163 | 164 | def eval_function!(state, path, args, max_reductions) when is_list(path) do 165 | eval_function!(state, Enum.join(path, "."), args_to_list(args), max_reductions) 166 | end 167 | 168 | def eval_function!(state, path, args, max_reductions) when is_binary(path) do 169 | state 170 | |> set!("__sandbox_args__", args_to_list(args)) 171 | |> eval!("return " <> path <> "(unpack(__sandbox_args__))", max_reductions) 172 | end 173 | 174 | @doc """ 175 | Create a compiled chunk of Lua code that can be transferred between Lua states, returned in an ok-error tuple. 176 | """ 177 | @spec chunk(lua_state(), lua_code()) :: {:ok, lua_chunk()} | {:error, any()} 178 | def chunk(state, code) do 179 | case :luerl.load(code, state) do 180 | {:ok, result, _state} -> 181 | {:ok, result} 182 | 183 | {:error, e1, e2} -> 184 | {:error, {e1, e2}} 185 | # {:error, reason} -> {:error, reason} 186 | end 187 | end 188 | 189 | @doc """ 190 | Same as `chunk/2`, but will return the raw result or raise a `RuntimeError`. 191 | """ 192 | 193 | @spec chunk!(lua_state(), lua_code()) :: lua_chunk() 194 | def chunk!(state, code) do 195 | {:ok, result} = chunk(state, code) 196 | result 197 | end 198 | 199 | @doc """ 200 | Runs a Lua string or chunk against a Lua state and returns a new Lua state in an ok-error tuple. 201 | """ 202 | @spec play(lua_state(), lua_code(), non_neg_integer()) :: {:ok, lua_state()} | {:error, any()} 203 | def play(state, code, max_reductions \\ @unlimited_reductions) do 204 | case :luerl_sandbox.run(code, state, max_reductions) do 205 | {:error, e} -> {:error, e} 206 | {_result, new_state} -> {:ok, new_state} 207 | end 208 | end 209 | 210 | @doc """ 211 | Same as `play/3`, but will return the raw result or raise a `RuntimeError`. 212 | """ 213 | @spec play!(lua_state(), lua_code(), non_neg_integer()) :: lua_state() 214 | def play!(state, code, max_reductions \\ @unlimited_reductions) do 215 | case :luerl_sandbox.run(code, state, max_reductions) do 216 | {:error, {:reductions, _n}} -> raise(@reduction_error) 217 | {_result, new_state} -> new_state 218 | end 219 | end 220 | 221 | @doc """ 222 | Runs a Lua file in the context of a Lua state and returns a new Lua state. 223 | """ 224 | @spec play_file!(lua_state(), String.t(), non_neg_integer()) :: lua_state() 225 | def play_file!(state, file_path, max_reductions \\ @unlimited_reductions) 226 | when is_binary(file_path) and is_integer(max_reductions) do 227 | code = File.read!(file_path) 228 | play!(state, code, max_reductions) 229 | end 230 | 231 | @doc """ 232 | Runs a Lua function defined in the given Lua state and returns a new Lua state. 233 | """ 234 | @spec play_function!(lua_state(), lua_path(), non_neg_integer()) :: lua_state() 235 | def play_function!(state, path, args \\ [], max_reductions \\ @unlimited_reductions) 236 | 237 | def play_function!(state, path, args, max_reductions) when is_list(path) do 238 | play_function!(state, Enum.join(path, "."), args_to_list(args), max_reductions) 239 | end 240 | 241 | def play_function!(state, path, args, max_reductions) when is_binary(path) do 242 | state 243 | |> set!("__sandbox_args__", args_to_list(args)) 244 | |> play!("return " <> path <> "(unpack(__sandbox_args__))", max_reductions) 245 | end 246 | 247 | @doc """ 248 | Runs a Lua string or chunk against the given Lua state and returns the result and the new Lua state in an ok-error tuple. 249 | """ 250 | 251 | @spec run(lua_state(), lua_code(), non_neg_integer()) :: 252 | {:ok, lua_state() | {lua_value(), lua_state()}} | {:error, any()} 253 | def run(state, code, max_reductions \\ @unlimited_reductions) do 254 | case :luerl_sandbox.run(code, state, max_reductions) do 255 | {:error, e} -> {:error, e} 256 | {[], new_state} -> {:ok, {nil, new_state}} 257 | {[{:tref, _} = table | _], new_state} -> {:ok, {:luerl.decode(table, new_state), new_state}} 258 | {[result | _], new_state} -> {:ok, {result, new_state}} 259 | end 260 | end 261 | 262 | @doc """ 263 | Same as `run/3`, but will return the raw `{result, state}` or raise a `RuntimeError`. 264 | """ 265 | @spec run!(lua_state(), lua_code(), non_neg_integer()) :: {lua_value(), lua_state()} 266 | def run!(state, code, max_reductions \\ @unlimited_reductions) do 267 | case :luerl_sandbox.run(code, state, max_reductions) do 268 | {:error, {:reductions, _n}} -> raise(@reduction_error) 269 | {[{:tref, _} = table], new_state} -> {:luerl.decode(table, new_state), new_state} 270 | {[result], new_state} -> {result, new_state} 271 | {[], new_state} -> {nil, new_state} 272 | end 273 | end 274 | 275 | @doc """ 276 | Runs a function defined in the the Lua state and returns the result and the new Lua state as `{result, state}`. 277 | Lua functions in the Lua state can be referenced by their `lua_path`, a string or list such as `math.floor` or `["math", "floor"]`. 278 | """ 279 | 280 | @spec run_function!(lua_state(), lua_path(), non_neg_integer()) :: {lua_value(), lua_state()} 281 | def run_function!(state, path, args \\ [], max_reductions \\ @unlimited_reductions) 282 | 283 | def run_function!(state, path, args, max_reductions) when is_list(path) do 284 | run_function!(state, Enum.join(path, "."), args_to_list(args), max_reductions) 285 | end 286 | 287 | def run_function!(state, path, args, max_reductions) when is_binary(path) do 288 | state 289 | |> set!("__sandbox_args__", args_to_list(args)) 290 | |> run!("return " <> path <> "(unpack(__sandbox_args__))", max_reductions) 291 | end 292 | 293 | @doc """ 294 | Sets a value in a Lua state and returns the modified state. If `force` is set to true, new tables will be created 295 | automatically if they missing from the given `lua_path`. 296 | """ 297 | @spec set!(lua_state(), lua_path(), any(), boolean()) :: lua_state() 298 | def set!(state, path, value, force \\ false) 299 | 300 | def set!(state, path, value, force) when is_binary(path) do 301 | set!(state, String.split(path, "."), value, force) 302 | end 303 | 304 | def set!(state, path, value, false) when is_list(path) do 305 | :luerl.set_table(path, value, state) 306 | end 307 | 308 | def set!(state, path, value, true) when is_list(path) do 309 | :luerl.set_table(path, value, build_missing_tables(state, path)) 310 | end 311 | 312 | @doc """ 313 | Gets a value from a Lua state. 314 | """ 315 | @spec get!(lua_state(), lua_path()) :: lua_value() 316 | def get!(state, path) when is_list(path) do 317 | {result, _s} = :luerl.get_table(path, state) 318 | result 319 | end 320 | 321 | def get!(state, path) when is_binary(path) do 322 | get!(state, String.split(path, ".")) 323 | end 324 | 325 | @doc """ 326 | Returns a Lua state modified to include an Elixir function, `elixir_eval_fun()`, at the given `lua_path()`. 327 | 328 | The `elixir_eval_fun()` takes two arguments, a Lua state and a list of calling arguments from Lua. 329 | Its return value is passed along to Lua. It will not mutate the Lua state against which it executes. 330 | """ 331 | @spec let_elixir_eval!(lua_state(), lua_path(), elixir_eval_fun()) :: 332 | lua_state() 333 | def let_elixir_eval!(state, name, fun) when is_function(fun) do 334 | value = lua_wrap_elixir_eval(fun) 335 | set!(state, name, value) 336 | end 337 | 338 | @doc """ 339 | Returns a Lua state modified to include an Elixir function, `elixir_play_fun()`, at the given `lua_path()`. 340 | 341 | The `elixir_play_fun()` takes two arguments, a Lua state and a list of calling arguments from Lua. 342 | It should return a new Lua state. 343 | 344 | This can be used to let Lua scripts use something like controlled inheritance, dynamically adding external functionality and settings. 345 | """ 346 | @spec let_elixir_play!(lua_state(), lua_path(), elixir_play_fun()) :: 347 | lua_state() 348 | def let_elixir_play!(state, path, fun) when is_function(fun) do 349 | value = lua_wrap_elixir_play(fun) 350 | set!(state, path, value) 351 | end 352 | 353 | @doc """ 354 | Returns a Lua state modified to include an Elixir function, `elixir_run_fun()`, at the given `lua_path()`. 355 | 356 | The `elixir_run_fun()` takes two arguments, a Lua state and a list of calling arguments from Lua. 357 | It should return a tuple holding the result intended for the calling Lua function alongside a new Lua state. 358 | """ 359 | @spec let_elixir_run!(lua_state(), lua_path(), elixir_run_fun()) :: 360 | lua_state() 361 | def let_elixir_run!(state, name, fun) when is_function(fun) do 362 | value = lua_wrap_elixir_run(fun) 363 | set!(state, name, value) 364 | end 365 | 366 | @doc false 367 | def reduction_error(), do: @reduction_error 368 | 369 | # --- private functions --- 370 | 371 | # lua state is unchanged, result returned 372 | defp lua_wrap_elixir_eval(fun) do 373 | fn args, state -> 374 | result = fun.(state, args) 375 | {[result], state} 376 | end 377 | end 378 | 379 | # lua result and state returned 380 | defp lua_wrap_elixir_run(fun) do 381 | fn args, state -> 382 | {result, new_state} = fun.(state, args) 383 | {[result], new_state} 384 | end 385 | end 386 | 387 | # lua state is changed 388 | defp lua_wrap_elixir_play(fun) do 389 | fn args, state -> 390 | new_state = fun.(state, args) 391 | {[], new_state} 392 | end 393 | end 394 | 395 | defp args_to_list(args) when is_list(args) do 396 | args 397 | end 398 | 399 | defp args_to_list(args) do 400 | [args] 401 | end 402 | 403 | defp build_missing_tables(state, path, path_string \\ nil) 404 | 405 | defp build_missing_tables(state, [], _path_string) do 406 | state 407 | end 408 | 409 | defp build_missing_tables(state, [name | path_remaining], path_string) do 410 | next_path_string = 411 | case path_string do 412 | nil -> name 413 | _ -> path_string <> "." <> name 414 | end 415 | 416 | next_state = 417 | case get!(state, next_path_string) do 418 | nil -> set!(state, next_path_string, []) 419 | _ -> state 420 | end 421 | 422 | build_missing_tables(next_state, path_remaining, next_path_string) 423 | end 424 | end 425 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Sandbox.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.5.0" 5 | def project do 6 | [ 7 | app: :sandbox, 8 | version: @version, 9 | elixir: "~> 1.9", 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps(), 12 | description: description(), 13 | name: "sandbox", 14 | source_url: "https://github.com/darkmarmot/elixir_sandbox", 15 | homepage_url: "https://github.com/darkmarmot/elixir_sandbox", 16 | docs: docs(), 17 | author: "Scott Southworth", 18 | package: package() 19 | ] 20 | end 21 | 22 | # Run "mix help compile.app" to learn about applications. 23 | def application do 24 | [ 25 | extra_applications: [:logger] 26 | ] 27 | end 28 | 29 | defp package() do 30 | [ 31 | name: "sandbox", 32 | maintainers: ["Scott Southworth"], 33 | licenses: ["Apache 2.0"], 34 | links: %{"GitHub" => "https://github.com/darkmarmot/elixir_sandbox"} 35 | ] 36 | end 37 | 38 | defp description() do 39 | """ 40 | Sandbox provides restricted, isolated scripting environments for Elixir through the use of Lua by wrapping 41 | Robert Virding's Luerl library. 42 | """ 43 | end 44 | 45 | defp docs() do 46 | [ 47 | main: "Sandbox", 48 | name: "Sandbox", 49 | source_ref: "v#{@version}", 50 | canonical: "http://hexdocs.pm/sandbox", 51 | source_url: "https://github.com/darkmarmot/elixir_sandbox", 52 | extras: [] 53 | ] 54 | end 55 | 56 | # Run "mix help deps" to learn about dependencies. 57 | defp deps do 58 | [ 59 | {:luerl, "~> 0.4.0"}, 60 | {:ex_doc, "~> 0.21", only: :dev, runtime: false}, 61 | {:dialyxir, "~> 1.0.0-rc.6", only: [:dev], runtime: false} 62 | ] 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "earmark": {:hex, :earmark, "1.3.4", "52aba89c60529284df5fc18adc4c808b7346e72668bc2fb2b68d7394996c4af8", [:mix], [], "hexpm"}, 4 | "erlex": {:hex, :erlex, "0.2.4", "23791959df45fe8f01f388c6f7eb733cc361668cbeedd801bf491c55a029917b", [:mix], [], "hexpm"}, 5 | "ex_doc": {:hex, :ex_doc, "0.21.1", "5ac36660846967cd869255f4426467a11672fec3d8db602c429425ce5b613b90", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "luerl": {:hex, :luerl, "0.4.0", "6d3191f529286b77b765e790a70cc36b6b27d91e97363afa89e0b7394f5116bd", [:rebar3], [], "hexpm"}, 7 | "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, 10 | } 11 | -------------------------------------------------------------------------------- /test/lua/animal.lua: -------------------------------------------------------------------------------- 1 | 2 | animal_voices = {bunny="silence", cow="moo", cat="meow", dog="woof"} 3 | counter = 0 4 | sleeping = false 5 | 6 | function sleep() 7 | sleeping = true 8 | end 9 | 10 | function speak(animal) 11 | return animal_voices[animal] 12 | end 13 | 14 | function voices() 15 | return animal_voices 16 | end 17 | 18 | function waste_cycles(n) 19 | local t = 0 20 | for i=1,n do 21 | t = t + i 22 | end 23 | return t 24 | end 25 | 26 | function talk(n) 27 | counter = counter + n 28 | return counter 29 | end -------------------------------------------------------------------------------- /test/lua/mobility.lua: -------------------------------------------------------------------------------- 1 | --function mobility () 2 | -- local state = {x = 0, y = 0} 3 | -- 4 | -- local move = function (x, y) 5 | -- state.x = state.x + x 6 | -- state.y = state.y + y 7 | -- end 8 | -- 9 | -- local get_position = function () 10 | -- return {x = state.x, y = state.y} 11 | -- end 12 | -- 13 | -- return { 14 | -- move = move, 15 | -- get_position = get_position 16 | -- } 17 | --end 18 | 19 | m = inherit_mobility("meow dog") 20 | r = move(7) 21 | f = feels("happy") 22 | 23 | return f -------------------------------------------------------------------------------- /test/sandbox_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SandboxTest do 2 | use ExUnit.Case 3 | doctest Sandbox 4 | 5 | def mobility(state, _args) do 6 | state 7 | |> Sandbox.set!("x", 3) 8 | |> Sandbox.set!("feeling", "poo") 9 | |> Sandbox.set!("hunger", 7) 10 | |> Sandbox.let_elixir_run!("move", &SandboxTest.move/2) 11 | |> Sandbox.let_elixir_eval!("feels", fn _state, [p | _] -> to_string(p) <> " feels" end) 12 | end 13 | 14 | def move(state, [d | _rest]) do 15 | x = state |> Sandbox.get!("x") 16 | result = x + d 17 | new_state = state |> Sandbox.set!("x", result) 18 | {result, new_state} 19 | end 20 | 21 | test "can set value" do 22 | output = 23 | Sandbox.init() 24 | |> Sandbox.set!("some_variable", "some_value") 25 | |> Sandbox.eval!("return some_variable") 26 | 27 | assert output == "some_value" 28 | end 29 | 30 | test "can set value at path" do 31 | output = 32 | Sandbox.init() 33 | |> Sandbox.set!("some_table", []) 34 | |> Sandbox.set!(["some_table", "some_variable"], "some_value") 35 | |> Sandbox.eval!("return some_table.some_variable") 36 | 37 | assert output == "some_value" 38 | end 39 | 40 | test "can set value at path with dot notation" do 41 | output = 42 | Sandbox.init() 43 | |> Sandbox.set!("some_table", []) 44 | |> Sandbox.set!("some_table.some_variable", "some_value") 45 | |> Sandbox.eval!("return some_table.some_variable") 46 | 47 | assert output == "some_value" 48 | end 49 | 50 | test "can set value at path with dot notation and fail with missing table" do 51 | assert catch_error( 52 | Sandbox.init() 53 | |> Sandbox.set!("some_table.some_variable", "some_value") 54 | |> Sandbox.eval!("return some_table.some_variable") 55 | ) 56 | end 57 | 58 | test "can set value at path with dot notation and force missing table creation" do 59 | output = 60 | Sandbox.init() 61 | |> Sandbox.set!("some_table.some_variable", "some_value", true) 62 | |> Sandbox.eval!("return some_table.some_variable") 63 | 64 | assert output == "some_value" 65 | end 66 | 67 | test "can set value at path and not need forced table creation" do 68 | output = 69 | Sandbox.init() 70 | |> Sandbox.set!("some_table", [], true) 71 | |> Sandbox.set!(["some_table", "some_variable"], "some_value", true) 72 | |> Sandbox.eval!("return some_table.some_variable") 73 | 74 | assert output == "some_value" 75 | end 76 | 77 | test "can get value at path with get!" do 78 | output = 79 | Sandbox.init() 80 | |> Sandbox.set!("some_table", []) 81 | |> Sandbox.set!("some_table.some_variable", "some_value") 82 | |> Sandbox.get!(["some_table", "some_variable"]) 83 | 84 | assert output == "some_value" 85 | end 86 | 87 | test "can call function at path" do 88 | output = 89 | Sandbox.init() 90 | |> Sandbox.play_file!("test/lua/animal.lua") 91 | |> Sandbox.eval_function!(["speak"], ["bunny"]) 92 | 93 | assert output == "silence" 94 | end 95 | 96 | test "can call function at path as string" do 97 | output = 98 | Sandbox.init() 99 | |> Sandbox.play_file!("test/lua/animal.lua") 100 | |> Sandbox.eval_function!("speak", ["cow"], 0) 101 | 102 | assert output == "moo" 103 | end 104 | 105 | test "can call function returning an object" do 106 | output = 107 | Sandbox.init() 108 | |> Sandbox.play_file!("test/lua/animal.lua") 109 | |> Sandbox.eval_function!("voices", [], 0) 110 | 111 | assert output == [ 112 | {"bunny", "silence"}, 113 | {"cat", "meow"}, 114 | {"cow", "moo"}, 115 | {"dog", "woof"} 116 | ] 117 | end 118 | 119 | test "can call function at path with single arg wrapped as array" do 120 | output = 121 | Sandbox.init() 122 | |> Sandbox.play_file!("test/lua/animal.lua") 123 | |> Sandbox.eval_function!("speak", "dog", 100_000) 124 | 125 | assert output == "woof" 126 | end 127 | 128 | test "can handle chunks" do 129 | state = Sandbox.init() 130 | 131 | code = 132 | state 133 | |> Sandbox.chunk!("return 7") 134 | 135 | output = Sandbox.eval!(state, code) 136 | assert output == 7 137 | end 138 | 139 | test "can chunk against file defined functions" do 140 | state = Sandbox.init() 141 | 142 | code = 143 | state 144 | |> Sandbox.chunk!("return 7") 145 | 146 | output = Sandbox.eval!(state, code) 147 | assert output == 7 148 | end 149 | 150 | test "can expose Elixir function" do 151 | state = Sandbox.init() 152 | 153 | output = 154 | state 155 | |> Sandbox.let_elixir_eval!("puppy", fn _state, p -> to_string(p) <> " is cute" end) 156 | |> Sandbox.eval_function!("puppy", "dog", 10000) 157 | 158 | assert output == "dog is cute" 159 | end 160 | 161 | test "can expose Elixir function that reaches reduction limit" do 162 | state = Sandbox.init() 163 | 164 | long_function = fn -> 165 | state 166 | |> Sandbox.let_elixir_eval!("puppy", fn _state, p -> 167 | Enum.map(1..10000, fn _ -> to_string(p) <> " is cute" end) 168 | |> List.last() 169 | end) 170 | |> Sandbox.eval_function!("puppy", "dog", 2000) 171 | end 172 | 173 | assert_raise(RuntimeError, Sandbox.reduction_error(), long_function) 174 | end 175 | 176 | test "can play a Lua function that updates the Lua state" do 177 | state = Sandbox.init() 178 | 179 | output = 180 | state 181 | |> Sandbox.play_file!("test/lua/animal.lua") 182 | |> Sandbox.play_function!(["talk"], 4, 10000) 183 | |> Sandbox.get!("counter") 184 | 185 | assert output == 4 186 | end 187 | 188 | test "can play a Lua function without arguments that updates the Lua state" do 189 | state = Sandbox.init() 190 | 191 | output = 192 | state 193 | |> Sandbox.play_file!("test/lua/animal.lua") 194 | |> Sandbox.play_function!("sleep") 195 | |> Sandbox.get!("sleeping") 196 | 197 | assert output == true 198 | end 199 | 200 | test "can run Lua to update the Lua state with no return value" do 201 | state = Sandbox.init() 202 | 203 | {:ok, {_result, new_state}} = 204 | state 205 | |> Sandbox.play_file!("test/lua/animal.lua") 206 | |> Sandbox.run("sleeping = true") 207 | 208 | output = new_state |> Sandbox.get!("sleeping") 209 | 210 | assert output == true 211 | end 212 | 213 | test "can run a Lua function that updates the Lua state" do 214 | state = Sandbox.init() 215 | 216 | {output, _new_state} = 217 | state 218 | |> Sandbox.play_file!("test/lua/animal.lua") 219 | |> Sandbox.run_function!("talk", 4, 10000) 220 | 221 | assert output == 4 222 | end 223 | 224 | test "can chunk a Lua function and then use it" do 225 | state = Sandbox.init() 226 | code = "function growl(n)\nreturn n + 2\nend" 227 | chunk = Sandbox.chunk!(state, code) 228 | 229 | output = 230 | state 231 | |> Sandbox.play!(chunk) 232 | |> Sandbox.eval_function!("growl", 7) 233 | 234 | assert output == 9 235 | end 236 | 237 | test "can play functionality to state through Elixir" do 238 | state = Sandbox.init() 239 | 240 | output = 241 | state 242 | |> Sandbox.let_elixir_play!("inherit_mobility", &SandboxTest.mobility/2) 243 | |> Sandbox.eval_file!("test/lua/mobility.lua") 244 | 245 | assert output == "happy feels" 246 | end 247 | 248 | test "can play functionality to state through Elixir with ok-error tuple" do 249 | state = Sandbox.init() 250 | 251 | {:ok, output} = 252 | state 253 | |> Sandbox.let_elixir_play!("inherit_mobility", &SandboxTest.mobility/2) 254 | |> Sandbox.eval_file("test/lua/mobility.lua") 255 | 256 | assert output == "happy feels" 257 | end 258 | 259 | test "can play functionality to state through Elixir with ok-error tuple and hit reduction limit" do 260 | state = Sandbox.init() 261 | 262 | output = 263 | state 264 | |> Sandbox.let_elixir_play!("inherit_mobility", &SandboxTest.mobility/2) 265 | # |> Sandbox.eval_function!("waste_cycles", [1000]) 266 | |> Sandbox.eval_file("test/lua/mobility.lua", 1000) 267 | 268 | assert {:error, {:reductions, _}} = output 269 | end 270 | 271 | test "can get value" do 272 | output = 273 | Sandbox.init() 274 | |> Sandbox.set!("some_variable", "some_value") 275 | |> Sandbox.get!("some_variable") 276 | 277 | assert output == "some_value" 278 | end 279 | 280 | test "can get value from unsafe init" do 281 | output = 282 | Sandbox.unsafe_init() 283 | |> Sandbox.set!("some_variable", "some_value") 284 | |> Sandbox.get!("some_variable") 285 | 286 | assert output == "some_value" 287 | end 288 | end 289 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------