Build an FTP Server with Ranch in 30 Minutes

2012 14 Nov

Last week I was speaking at the London Erlang Factory Lite where I presented a live demonstration of building an FTP server using Ranch. As there was no slide, you should use this article as a reference instead.

The goal of this article is to showcase how to use Ranch for writing a network protocol implementation, how Ranch gets out of the way to let you write the code that matters, and the common techniques used when writing servers.

Let's start by creating an empty project. Create a new directory and then open a terminal into that directory. The first step is to add Ranch as a dependency. Create the rebar.config file and add the following 3 lines.

{deps, [
    {ranch, ".*", {git, "git://github.com/extend/ranch.git", "master"}}
]}.

This makes your application depend on the last Ranch version available on the master branch. This is fine for development, however when you start pushing your application to production you will want to revisit this file to hardcode the exact version you are using, to make sure you run the same version of dependencies in production.

You can now fetch the dependencies.

$ rebar get-deps
==> ranch_ftp (get-deps)
Pulling ranch from {git,"git://github.com/extend/ranch.git","master"}
Cloning into 'ranch'...
==> ranch (get-deps)

This will create a deps/ folder containing Ranch.

We don't actually need anything else to write the protocol code. We could make an application for it, but this isn't the purpose of this article so let's just move on to writing the protocol itself. Create the file ranch_ftp_protocol.erl and open it in your favorite editor.

$ vim ranch_ftp_protocol.erl

Let's start with a blank protocol module.

-module(ranch_ftp_protocol).
-export([start_link/4, init/3]).

start_link(ListenerPid, Socket, Transport, Opts) ->
    Pid = spawn_link(?MODULE, init, [ListenerPid, Socket, Transport]),
    {ok, Pid}.

init(ListenerPid, Socket, Transport) ->
    io:format("Got a connection!~n"),
    ok.

When Ranch receives a connection, it will call the start_link/4 function with the listener's pid, socket, transport module to be used, and the options we define when starting the listener. We don't need options for the purpose of this article, so we don't pass them to the process we are creating.

Let's open a shell and start a Ranch listener to begin accepting connections. We only need to call one function. You should probably open it in another terminal and keep it open for convenience. If you quit the shell you will have to repeat the commands to proceed.

Also note that you need to type c(ranch_ftp_protocol). to recompile and reload the code for the protocol. You do not need to restart any process however.

$ erl -pa ebin deps/*/ebin
Erlang R15B02 (erts-5.9.2) [source] [64-bit] [smp:4:4] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.9.2  (abort with ^G)
1> application:start(ranch).
ok
2> ranch:start_listener(my_ftp, 10,
       ranch_tcp, [{port, 2121}],
       ranch_ftp_protocol, []).
{ok,<0.40.0>}

This starts a listener named my_ftp that runs your very own ranch_ftp_protocol over TCP, listening on port 2121. The last argument is the options given to the protocol that we ignored earlier.

To try your code, you can use the following command. It should be able to connect, the server will print a message in the console, and then the client will print an error.

$ ftp localhost 2121

Let's move on to actually writing the protocol.

Once you have created the new process and returned the pid, Ranch will give ownership of the socket to you. This requires a synchronization step though.

init(ListenerPid, Socket, Transport) ->
    ok = ranch:accept_ack(ListenerPid),
    ok.

Now that you acknowledged the new connection, you can use it safely.

When an FTP server accepts a connection, it starts by sending a welcome message which can be one or more lines starting with the code 200. Then the server will wait for the client to authenticate the user, and if the authentication went successfully, which it will always do for the purpose of this article, it will reply with a 230 code.

init(ListenerPid, Socket, Transport) ->
    ok = ranch:accept_ack(ListenerPid),
    Transport:send(Socket, <<"200 My cool FTP server welcomes you!\r\n">>),
    {ok, Data} = Transport:recv(Socket, 0, 30000),
    auth(Socket, Transport, Data).

auth(Socket, Transport, <<"USER ", Rest/bits>>) ->
    io:format("User authenticated! ~p~n", [Rest]),
    Transport:send(Socket, <<"230 Auth OK\r\n">>),
    ok.

As you can see we don't need complex parsing code. We can simply match on the binary in the argument!

Next we need to loop receiving data commands and optionally execute them, if we want our server to become useful.

We will replace the ok. line with the call to the following function. The new function is recursive, each call receiving data from the socket and sending a response. For now we will send an error response for all commands the client sends.

loop(Socket, Transport) ->
    case Transport:recv(Socket, 0, 30000) of
        {ok, Data} ->
            handle(Socket, Transport, Data),
            loop(Socket, Transport);
        {error, _} ->
            io:format("The client disconnected~n")
    end.

handle(Socket, Transport, Data) ->
    io:format("Command received ~p~n", [Data]),
    Transport:send(Socket, <<"500 Bad command\r\n">>).

With this we are almost ready to start implementing commands. But with code like this we might have errors if the client doesn't send just one command per packet, or if the packets arrive too fast, or if a command is split over multiple packets.

To solve this, we need to use a buffer. Each time we receive data, we will append to the buffer, and then check if we have received a command fully before running it. The code could look similar to the following.

loop(Socket, Transport, Buffer) ->
    case Transport:recv(Socket, 0, 30000) of
        {ok, Data} ->
			Buffer2 = << Buffer/binary, Data/binary >>,
			{Commands, Rest} = split(Buffer2),
			[handle(Socket, Transport, C) || C <- Commands],
            loop(Socket, Transport, Rest);
        {error, _} ->
            io:format("The client disconnected~n")
    end.

The implementation of split/1 is left as an exercice to the reader. You will also probably want to handle the QUIT command, which must stop any processing and close the connection.

The attentive reader will also take note that in the case of text- based protocols where commands are separated by line breaks, you can set an option using Transport:setopts/2 and have all the buffering done for you for free by Erlang itself.

As you can surely notice by now, Ranch allows us to build network applications by getting out of our way entirely past the initial setup. It lets you use the power of binary pattern matching to write text and binary protocol implementations in just a few lines of code.