A basic recipe for an Elixir SSL server

By: on January 23, 2018

In this post, we’ll first try out Erlang’s SSL application interactively and then put together a simple Elixir SSL server OTP application using the Supervisor and GenServer behaviours.

Preparation

First of all, we’ll create a self-signed certificate:

mkdir foo
cd foo
openssl genrsa -out key.pem 1024
openssl req -new -key key.pem -out request.pem # (using default values)
openssl x509 -req -days 30 -in request.pem -signkey key.pem -out certificate.pem

Next, we’ll run Elixir via Docker, mounting the directory with our self-signed certificate.

docker run -it -v /path/to/foo:/e --rm elixir bash

We’ll refer to this shell as iex-1. Let’s further prepare iex-1:

cd /e        # our directory with the certificate, mounted from the host
iex          # invoke the elixir repl
:ssl.start() # start the erlang ssl application

In another terminal window, we’ll run a second shell, iex-2, in the same container. Find out the container id via

docker ps

and then

docker exec -it cbfa9c5fb6fe bash # substitute correct id

In iex-2, run:

iex
:ssl.start()

Interactive SSL

In iex-1, we’ll create an SSL listen socket and wait for a client to connect to the underlying TCP socket:

 {:ok, l} = :ssl.listen(7000, [certfile: "certificate.pem", keyfile: "key.pem", reuseaddr: :true, packet: 4])
 {:ok, s} = :ssl.transport_accept(l) # will block until a client connects to the listen socket

By default, ssl sockets run in ‘active’ mode, which means that all incoming data will be forwarded to the process that owns the respective socket. In this case, the iex shell process is the owner. The option packet: 4 means that all messages are preceeded by a four-byte length header indicating the size of the message. With this option, Erlang’s gen_tcp module provides automatic defragmentation. If both client and server are Erlang/elixir processes, :erlang.term_to_binary / binary_to_term can be used to send arbitrary terms over the connection.

In iex-2, we’ll connect a client to the listen socket

{:ok, s} = :ssl.connect('localhost', 7000, [packet: 4], :infinity) # caution: 'localhost', not "localhost"!

Back in iex-1, we can complete the SSL handshake

:ok = :ssl.ssl_accept(s)

It is now possible to send messages.

In iex-2:

:ssl.send(s, "ping")

In iex-1, we print the received message

flush()

and send a reply

:ssl.send(s, "pong")

In iex-2, we print the reply

flush()

and then close the socket

:ssl.close(s)

You can flush() iex-1 to see the :ssl_closed message.
You can quit iex by pressing ctrl-\.

OTP Application

The next step is to make a basic OTP Application that opens an SSL listen socket and spawns a handler process for each incoming connection.

We will create a new application project with Elixir’s mix tool and copy the self-signed certificate into it:

docker run -it -v /path/to/foo:/e --rm elixir bash
cd /e
mix new demo
cp certificate.pem key.pem demo
cd demo

On the host machine, we can now edit foo/demo/lib/demo.ex.
In demo.ex, we will create three modules: DemoApp, ConnectionHandlerFactory and ConnectionHandler.

The code for DemoApp is:

defmodule DemoApp do
 use Application

 def start(_type, _args) do
  {:ok, l} = :ssl.listen(7000,[certfile: "certificate.pem", keyfile: "key.pem", reuseaddr: :true, active: :true, packet: 4])
  {:ok, pid} = ConnectionHandlerFactory.start_link(l)
  {:ok, _} = ConnectionHandlerFactory.start_child()
  {:ok, pid}
 end
end

This opens an SSL listen socket, starts the ConnectionHandlerFactory (handing it the socket) and asks the ConnectionHandlerFactory to start a child process.

The code for ConnectionHandlerFactory is:

defmodule ConnectionHandlerFactory do
 use Supervisor

 def start_link(socket) do
  Supervisor.start_link(__MODULE__, socket, name: __MODULE__)
 end

 def init(socket) do
  flags = %{:strategy => :simple_one_for_one, :intensity => 1, :period => 5}
  specs = [%{
   :id => :connectionHandlerFactory,
   start: {ConnectionHandler, :start_link, [socket]},
   restart: :temporary,
   shutdown: :brutal_kill,
   type: :worker,
   modules: [ConnectionHandler]
  }]
  {:ok, {flags, specs}}
 end

 def start_child() do
  Supervisor.start_child(__MODULE__, [])
 end
end

ConnectionHandlerFactory is a :simple_one_for_one supervisor, i.e. it can have many child processes of exactly the same type. Elixir offers some convenience functions for creating child specs, but here, we’ve assembled the spec explicitly. In order for other processes to be able to call start_child(), ConnectionHandlerFactory has to be a registered process. Registration happens through the name: __MODULE__ option in the call to Supervisor.start_link/3.
As each supervised ConnectionHandler is responsible for a single connection, the restart type is :temporary. When such a child terminates, the supervisor will not attempt to restart it.

The code for the third and final module is:

defmodule ConnectionHandler do
 use GenServer

 def start_link(sock) do
  GenServer.start_link(__MODULE__, [sock])
 end

 def init([sock]) do
  {:ok, sock, 0}
 end

 def handle_info(:timeout, l) do
  {:ok, s} = :ssl.transport_accept(l)    # wait for a client to connect to the listen socket
  ConnectionHandlerFactory.start_child() # spawn another connection handler
  :ok = :ssl.ssl_accept(s)               # complete the handshake
  {:noreply, s}
 end

 def handle_info({:ssl, sslsocket, data}, state) do
  :ssl.send(sslsocket, data)
  {:noreply, state}
 end

 def handle_info({:ssl_closed, _sslsocket}, state) do
  {:stop, :normal, state}
 end

 def handle_info({:ssl_error, _sslsocket, _reason}, state) do
  {:stop, :normal, state}
 end
end

ConnectionHandler is a GenServer. However, we are only interested in the init/1 and handle_info/2 callbacks. Elixir provides default implementations for the other callbacks.
The start_link/1 function spawns a new ConnectionHandler process without registering it.
The init/1 function gets given the listen socket, which becomes the sole state of the GenServer. The ,0 in {:ok, sock, 0} is a timeout which is triggered immediately and stipulates a call to handle_info(:timeout, state). The corresponding handle_info clause waits for a connection on the listen socket. Once a client has connected, a new ConnectionHandler is started through a call to the ConnectionHandlerFactory. This allows for multiple clients to connect concurrently. Finally, the SSL handshake is completed and the resulting socket becomes the state of the GenServer. As the GenServer is the owner of the socket and the socket is running in active mode, the socket will send ‘out-of-band’ messages to the GenServer, which will be passed to the handle_info callback.
Three kinds of messages can be expected from the socket: data has arrived, the socket has been closed, and an error has occured.
The current implementation of ConnectionHandler echoes data back to the sender and terminates when the socket is closed or an error has occurred.

Finally, we need to edit demo/mix.exs, so that our DemoApp will be started. Ensure that the project block looks as follows:

def project do
 [
  app: DemoApp,
  version: "0.1.0",
  elixir: "~> 1.5",
  start_permanent: Mix.env == :prod,
  deps: deps()
 ]
end

The application block needs to look like this:

def application do
 [
  mod: {DemoApp, []},
  applications: [:ssl],
  extra_applications: [:logger]
 ]
end

We can now run our application from within our docker shell:

iex -S mix

We can test DemoApp by repeating the iex-2 commands in this new iex.

Next steps

That’s it. Possible next steps are setting up a more serious SSL configuration, adding authentication and doing something interesting in the ConnectionHandler.

Share

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*