Table of Contents

Update: I’ve added an update to the security section.

Today I released something I have been working on for a little while. It’s basically an IEx instance you can access from your browser.

The problem I was trying to solve is one I’ve had while learning Elixir myself: I would often be reading Dave Thomas’s Programming Elixir book on my iPad, and at somepoint I’d want to try out some Elixir code. Obviously on an iPad I can’t install the runtime, so I was left with no other option than to open my laptop. Hopefully this project will provide an alternative for people on mobile devices.

So, how was the project implemented? It’s actually quite simple. It’s a Phoenix app, without database, using CodeMirror on the frontend side of things for code editing.

But in order to bring the whole idea to completion, I had to solve the following problems:

  • Evaluate the code sent by the user
  • Capture and return all the output, including errors
  • Find a pretty domain
  • Code the frontend
  • Choose an online code editor
  • Implement a syntax highlighter

Let’s look at the first point.

IO in Elixir

With Elixir’s focus on distributed computing, it should come as no surprise that IO itself can be thought of as a distributed process.

So, how does IO work in Elixir?

Basically every IO function either implicitly or explicitly writes to an IO device. In the case of IO.inspect/2, for example, you do it implicitly; its counterpart IO.inspect/3 receives an additional argument: the device.

Some examples of devices are :stdio and :stderr, but a device can be any Elixir process, and can be passed around as a pid (which is a first-class data type in Elixir) or an atom representing a process.

Another example is IO.puts/2. If you look at its definition, you’ll see the first argument it accepts has a default value, group_leader().

In Erlang, a process is always part of a group, and a group always has a group leader. All IO from the group is sent to the group leader. The group leader is basically the default IO device, and it can be configured by calling :erlang.group_leader/2.

In our case, we want to somehow save all output, and then be able to return it to the browser. How do we go about doing that?

Turns out Elixir has a very nice module that exposes a string as an IO device. The module is aptly called StringIO, and it allows us to save and retrieve all data that has been sent to the device.

Combining these two concepts together, the group leader and StringIO, allows us to achieve our goal: redirect all output to a string, store it, and return it:

{:ok, device} = StringIO.open ""
:erlang.group_leader(device, self)

By the way, this stuff is explained quite well on this page of the documentation.

Evaluating code

The next interesting thing happening in this project is it has to evaluate the code that the user sends at runtime.

Again, Elixir offers a simple solution in the form of Code.eval_string/3. It has only one mandatory argument, the code as a string, and returns a tuple of the form {result, bindings}. We are only interested in the result.

result =
  content
  |> Code.eval_string
  |> elem(0)

IO.inspect result

Note that we use IO.inspect here, instead of IO.puts, as that allows us to leverage the existing implementations of the Inspect protocol.

This gets us almost all the way there. But what happens if the user’s code causes an exception? Ideally, we should recover from the exception and output the error message, same as it would happen on IEx.

Let’s wrap our code in a try/rescue block:

    try do
      result =
        content
        |> Code.eval_string
        |> elem(0)

      IO.inspect result
    rescue
      exception -> IO.inspect Exception.message(exception)
    end

And that’s all. With our backend code in place, let’s now move to the frontend.

Elixir syntax in CodeMirror

CodeMirror is an online editor for code. It is distributed via NPM in a modular way, each module in its own file. This meant that I could use bower for the package, and in my bower.json only require the files that I needed, thereby avoiding bloat and saving a lot of space in the final application.js:

{
  "name": "TryElixir",
  "dependencies": {
    "codemirror-elixir": "asymmetric/codemirror-elixir",
    "fetch": "^0.11.0",
    "es6-promise": "^3.2.1",
    "bootstrap": "^3.3.6",
    "hint.css": "^2.2.0"
  },
  "overrides": {
    "codemirror": {
      "main": [
        "lib/codemirror.js",
        "lib/codemirror.css",
        "theme/material.css",
        "addon/mode/simple.js"
      ]
    },
    "bootstrap": {
      "main": [
        "dist/css/bootstrap.css"
      ],
      "dependencies": {}
    }
  }
}

There was no syntax highlighter file for Elixir, so I had to build one. It’s still a work in progress, and PRs are very welcome. Actually, CodeMirror offes 2 ways of defining syntaxes (what it calls “modes”): one is RegEx based, and it’s very simple. The other one is to define a proper lexer (here is the one for Ruby for example) and although it’s the recommended way, it was definitely too much work for my use-case. But if you’re in that kind of thing, there’s an opportunity for you!

Security

(Note: This section was updated on May 11 2016)

Given that this project allows you to execute arbitrary code, just how much risk is there?

Initially, I thought that Heroku’s ephemeral filesystem would be enough: users wouldn’t be able to permanently affect the application, even if they somehow managed to run “malicious” code.

And that’s true, but I was greatly underestimating the amount of damage that can be done when you can run arbitrary code.

For example, you can shut down the VM with :erlang.halt, or restart it with :init.reboot. Additionally, you can run code that will never stop executing, while stealing every CPU cycle available.

To address these problems, I’ve come up with two solutions: a blacklist of forbidden commands, and performing code evaluation inside an async Task .

Async tasks are particularly interesting. You can invoke one with Task.async(fn -> your_function() end), and afterwards call Task.yield(your_task, your_timeout) to send it off to do its work.

The key thing here is the timeout: after it expires, the Task is expected to have a value. If it has one, it’s returned; if it doesn’t, we can handle that however we want. In my case, I kill it brutally and just return a message to the user, informing them that the computation took too long.

Domain, hosting and DNS

I was very happy to have found a catchy TLD for the site. The registrar is Namecheap (affiliate link) and it only cost $0.89 for the first year!

The app is hosted on Heroku, and normally what you would do is point www.yourdomain.com to your-app.herokuapp.com. But I wanted to get rid of the www. subdomain, and it turns out you can’t do that with a lot of DNS providers. The reason is that CNAME records can only be created for subdomains. Heroku does not provide each app with a fixed IP obviously, so you can’t just create an A record and call it a day.

Some providers though do offer so-called ANAME/ALIAS records, bust most of them cost an order of magnitude more than the domain itself. That’s why I was very surprised to find out that CloudFlare offers their DNS services for free! So I proceeded to hand over DNS resolution for my domain to them (which by the way took something like 5 minutes!), configure the root CNAME and that was it! Now the domain is availale without that ugly www.!

TODOs

While I’m pretty happy with the result, there are definitely some ideas that are worth investigting.

It might be interesting to rewrite the app without Phoenix, as a barebones Plug app. The backend is extremely simple, the challenging part would be requiring frontend modules without using Brunch (or having to setup Brunch myself).

Another important thing to do would be to default to HTTPS, maybe using Let’s Encrypt.

And, last but not least, is the tutorial. Most other online interpreters double as intros to the language, with chapters, lessons and excercises. This is not the main focus of the project (as I said, I just wanted an easy way to try out code as I was learning), but if people find it useful, I might implement it.

And I think that’s all.

Have fun!