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:
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.
We’ll refer to this shell as iex-1. Let’s further prepare iex-1:
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
and then
In iex-2, run:
: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, 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
Back in iex-1, we can complete the SSL handshake
It is now possible to send messages.
In iex-2:
In iex-1, we print the received message
and send a reply
In iex-2, we print the reply
and then close the socket
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:
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:
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:
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:
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:
[
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:
[
mod: {DemoApp, []},
applications: [:ssl],
extra_applications: [:logger]
]
end
We can now run our application from within our docker shell:
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.