Why build a FTP server
I’ve just started learning F# and the application I decided to build needs a way of exposing a read-only directory structure/file system. I initially looked at WebDAV but it seemed unnecessarily complex for what I was trying to do, there also is apparently a compatibility issue between the windows implementation and the standard. FTP on the other hand is well documented, well supported and cross platform.
As it turns out building a FTP server that supports the minimum number of features is actually pretty simple. By implementing only ten of the many possible FTP commands I was able to build a FTP server that windows explorer and a number of FTP clients could happily use.
I was very surprised how easy building the server was. I’m sure I’ll use this approach for a number of different applications in the future. Hopefully I can convince you to do the same :)
Two things to note however; firstly this is obviously not meant to be a production grade server but it could certainly be taken to that extent if required. Secondly this is only my third day of F# so I may well have missed some F# tricks or done some things in a non-idiomatic way.
The FTP Protocol
FTP is a line based protocol like telnet. This is part of what makes implementing it so easy.
For example this is what an authentication conversation would look like
Client: [Connect to server] | |
Server: [220 Hello, welcome to …] | |
Client: USER usr123 | |
Server: 331 password required | |
Client: PASS abcde |
|
| Server: 230 logged in |
That is it, pretty simple, right?
For more details on the protocol you can take a look at RFC959 (http://www.ietf.org/rfc/rfc959.txt).
There are also many sites that explain the protocol in plain English. Here are three sites that I found particularly useful in getting started
- An Overview of the File Transfer Protocol - http://www.ncftp.com/libncftp/doc/ftp_overview.html
- List of raw FTP commands - http://www.nsftools.com/tips/RawFTP.htm
- FTP, File Transfer Protocol - http://www.networksorcery.com/enp/protocol/ftp.htm
The minimal set of FTP commands
To get the FTP server to support a few FTP clients (FileZilla, explorer and ftp.exe command line client) I had to implement the following commands
- USER - User name
- PASS - Password
- CWD - Change working directory
- PWD - Print working directory
- TYPE - Binary/text mode. I just ignored this command
- LIST/NLST - Print a directory listing
- PORT - Set the data port for an active data transfer
- RETR - Retrieve a file
- QUIT - Quit
Clearly there is a lot that I have not implemented and I may extend what I support (e.g. CDUP, PASSV etc) but for now this is all I need.
Code structure
The full source code is available on githib at https://github.com/andrevdm/FSharpFtpServer.
The code is structured as follows
- SocketExtensions – contains extension methods for the Socket class. This was copied from http://fssnip.net/1E
- SocketServer - The main server. This accepts new connections and creates a new ISocketHandler derived class to handle the requests.
- LineProtocolHandler - Implements ISocketHandler. Reads data off the socket and calls the abstract Handle method after a line is read. NB The way I’m reading one byte at a time is far from optimal but it is simple and works well enough for now.
- FtpHandler - inherits from LineProtocolHandler and implements the Handle method to handle each command line as it is received.
- IDirectoryProvider. Interface for classes exposing a directory. I.e. responsible for generating the directory listing, keeping track of the current directory and downloading files etc.
- FileSystemDirectoryProvider - a directory provider for exposing a real file system. (This still needs work).
The idea here is that it is easy to plug in different functionality, for example different directory providers. I’m using this server to expose a virtual file system (something like the Git file system, more about that in a future blog) so I’ve not put much effort into the FilySystemDirectoryProvider but it should be pretty simple to get it working correctly.
Code examples
The login conversation was show above this is how it looks in the code.
When a client connects the server sends a 220 “hello” command.
do! t.Send 220 "Hello" socket
Here Send is a method that takes the code (220), the sting (“Hello”) and the socket to respond to.
The logged in state is then managed by the loginState mutable state variable. This can have one of the following values
type ftpLoginState =
| ExpectUserName
| ExpectPassword
| LoggedIn
The incoming lines are sent to the Handle() method
override t.Handle( line, socket ) =
async{
if line = "QUIT" then
t.Stop()
else
match loginState with
| ExpectUserName -> do! t.HandleLoginUserName( line, socket )
| ExpectPassword -> do! t.HandleLoginPassword( line, socket )
| LoggedIn -> do! t.HandleCommand( line, socket )
| _ -> failwith ("unknown ftpLoginState " + loginState.ToString())
}
This method works as follows
- If the QUIT command is received then call the Stop() method to terminate the connection
- All other commands are then interpreted based on the current login state
- If expecting a user name call HandleLoginUserName
- If expecting a password call HandleLoginPassword
- If logged in the call the HandleCommand method which handles the main FTP commands
- Any other state is invalid
The HandleCommand method then responds to the rest of the FTP commands.
member t.HandleCommand( line, socket ) =
async{
let c,r = t.SplitCmd line
match c with
| "PORT" -> do! t.SetPort( r, socket )
| "NLST"
| "LIST" -> do! t.SendList( r, socket )
| "PWD" -> do! t.Send 257 ("\"" + dirProvider.CurrentPath + "\" is the current directory") socket
| "TYPE" -> do! t.Send 200 "ignored" socket
| "RETR" -> do! t.RetrieveFile( r, socket )
| "CWD" ->
if dirProvider.ChangeDir( r ) then do! t.Send 200 "directory changed" socket
else do! t.Send 552 "Invalid directory" socket
| _ -> do! t.Send 502 "Command not implemented" socket
}
The SplitCmd call is a simple method to split the received line.
member t.SplitCmd( line ) =
let m = Regex.Match( line, @"^(?<cmd>[^ ]+)( (?<rest>.*))?" )
if not m.Success then failwith ("invalid command: " + line)
(m.Groups.["cmd"].Value, m.Groups.["rest"].Value)
LIST / NLST
The LIST and NLST commands request the server to send a directory listing. Rather bizarrely there is no actual standard for this format. However most FTP server return the listing matching the standard unix ls format and most FTP clients expect this too.
Here is how the FileSystemDirectoryProvider returns the files and sub-directories of the current path
member t.List() =
let dir = new StringBuilder()
let info = new DirectoryInfo( physicalDirectory )
info.GetFiles()
|> Array.iter (fun f ->
dir.AppendFormat( "-r--r--r-- 1 owner group {1} 1970 01 01 {0}", f.Name, f.Length ).AppendLine() |> ignore )
info.GetDirectories()
|> Array.iter (fun d ->
dir.AppendFormat( "dr--r--r-- 1 owner group {1} 1970 01 01 {0}", d.Name, 0 ).AppendLine() |> ignore )
dir.ToString()
This listing is then sent back to the client by the FtpHandler, see the section on the PORT command below
member t.SendList( p, socket ) =
async{
do! t.Send 150 "Opening ASCII mode data connection for /bin/ls" socket
use sender = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
let endpoint = IPEndPoint(ipAddr, port)
sender.Connect( endpoint )
let! sent = sender.AsyncSend( Encoding.ASCII.GetBytes( dirProvider.List() ) )
do! t.Send 226 "Listing completed." socket
}
PORT and active mode data transfer
FTP uses different ports for its command and data communications. The command socket is always on the port that you connect on (typically port 21), data ports are created dynamically as required.
There are two ways in which ports are assigned
- In active mode the client creates a socket to listen for data on and tells the server the IP and port number
- In passive mode the client requests that server create a listener and return the IP and port. Passive mode is often preferred as it is simpler to support behind a firewall.
It was marginally simpler for me to implement active mode so for now that is all the server supports.
In active mode a data transfer like the LIST command described above would look like this
Client: PORT 127,0,0,1,4,1 | |
Server: 200 PORT command successful | |
Client: LIST | |
Server: 150 Opening ASCII mode data connection for /bin/ls | |
Server: [Data sent to 127.0.0.1:1026] | |
Server: 226 Listing completed |
So the client creates a listening socket and sends the details to the server. The port is split into high,low. The number = high * 256 + low. In this case 4*256+1 = 1026
The client then requests the list.
Finally the server then
- Tells the client that it is about to send the data
- Sends the data over the new data connection
- Tells the client that the data was sent successfully
In summary
I was able to build a simple FTP server in a few hundred lines of F#. I’m sure I’ll be using this code in a number of projects in the future. I hope that it will prove useful to you as well.
Learning F#
These are the resource that I’ve found particularly useful in learning F#.
F# Snipits - http://fssnip.net - There are some fantastic sample here. My base server code is based on http://fssnip.net/1E
Expert F# - http://www.amazon.com/Expert-F-Experts-Voice-NET/dp/1590598504 - A brilliant F# book.
No comments:
Post a Comment