Of course, since we need to read and write on sockets, having to do that again and again in every function wouldn't be very efficient. Therefore, we'll start by implementing functions to do that. For now, we won't handle errors nicely (yes, unwrap is evil).
Let's start with the write function:
use use std::net::TcpStream; use std::io::Write; fn send_cmd(stream: &mut TcpStream, code: ResultCode, message: &str) { let msg = if message.is_empty() { CommandNotImplemented = 502, format!("{}\r\n", code as u32) } else { format!("{} {}\r\n", code as u32, message) }; println!("<==== {}", msg); write!(stream, "{}", msg).unwrap() }
OK, there's nothing fancy nor difficult to understand here. However, take a look at this:
- Every message ends with "" in FTP
- Every message has to be followed by a whitespace if you want to add parameters or information.
This also works in the exact same way when a client sends us a command.
What? Did I forget to provide you the ResultCode type? Indeed, you're absolutely right. Here it is:
#[derive(Debug, Clone, Copy)] #[repr(u32)] #[allow(dead_code)] enum ResultCode { RestartMarkerReply = 110, ServiceReadInXXXMinutes = 120, DataConnectionAlreadyOpen = 125, FileStatusOk = 150, Ok = 200, CommandNotImplementedSuperfluousAtThisSite = 202, SystemStatus = 211, DirectoryStatus = 212, FileStatus = 213, HelpMessage = 214, SystemType = 215, ServiceReadyForNewUser = 220, ServiceClosingControlConnection = 221, DataConnectionOpen = 225, ClosingDataConnection = 226, EnteringPassiveMode = 227, UserLoggedIn = 230, RequestedFileActionOkay = 250, PATHNAMECreated = 257, UserNameOkayNeedPassword = 331, NeedAccountForLogin = 332, RequestedFileActionPendingFurtherInformation = 350, ServiceNotAvailable = 421, CantOpenDataConnection = 425, ConnectionClosed = 426, FileBusy = 450, LocalErrorInProcessing = 451, InsufficientStorageSpace = 452, UnknownCommand = 500, InvalidParameterOrArgument = 501, CommandNotImplemented = 502, BadSequenceOfCommands = 503, CommandNotImplementedForThatParameter = 504, NotLoggedIn = 530, NeedAccountForStoringFiles = 532, FileNotFound = 550, PageTypeUnknown = 551, ExceededStorageAllocation = 552, FileNameNotAllowed = 553, }
Yep, not very beautiful... This is the exact representation of all FTP code types (errors, information, warnings, and so on). We can't do much better here; we have to rewrite all code so that we can understand it when we receive it and are able to give the correct code corresponding to the clients' commands.
Now, I suppose, you can guess what's coming next. The enum Command of course! This time, we'll fulfill it while we move forward on to the implementation of the commands:
use std::io; use std::str; #[derive(Clone, Copy, Debug)] enum Command { Auth, Unknown(String), } impl AsRef<str> for Command { fn as_ref(&self) -> &str { match *self { Command::Auth => "AUTH", Command::Unknown(_) => "UNKN", } } } impl Command { pub fn new(input: Vec<u8>) -> io::Result<Self> { let mut iter = input.split(|&byte| byte == b' '); let mut command = iter.next().expect("command in
input").to_vec(); to_uppercase(&mut command); let data = iter.next(); let command = match command.as_slice() { b"AUTH" => Command::Auth, s => Command::Unknown(str::from_utf8(s).unwrap_or("").to_owned()), }; Ok(command) } }
OK, let's get through this code:
enum Command { Auth, Unknown(String), }
Every time we add a new command handling, we'll have to add a new variant to this enum. In case the command doesn't exist (or we haven't implemented it yet), Unknown will be returned with the command name. If the command is taking arguments, it'll be added just like we saw for Unknown. Let's take Cwd as an example:
enum Command { Auth, Cwd(PathBuf), Unknown(String), }
As you can see, Cwd contains a PathBuf. Cwd stands for change working directory and takes the path of the directory that the client wants to go to.
Of course, you'd need to update as_ref by adding the following line to the match block:
Command::Cwd(_) => "CWD",
And you'd need to update the new method implementation by adding the following line into the match block:
b"CWD" => Command::Cwd(data.map(|bytes| Path::new(str::from_utf8(bytes).unwrap()).to_path_buf()).unwrap()),
Now let's explain the AsRef trait implementation. It's very convenient when you want to write a generic function. Take a look at the following example:
fn foo<S: AsRef<str>>(f: S) { println!("{}", f.as_ref()); }
Thanks to this trait, as long as the type implements it, we can call as_ref on it. It's very useful in our case when sending messages to the client since we can just take a type implementing AsRef.
Now let's talk about the new method of the Command type:
pub fn new(input: Vec<u8>) -> io::Result<Self> { let mut iter = input.split(|&byte| byte == b' '); let mut command = iter.next().expect("command in input").to_vec(); to_uppercase(&mut command); let data = iter.next(); let command = match command.as_slice() { b"AUTH" => Command::Auth, s => Command::Unknown(str::from_utf8(s).unwrap_or("").to_owned()), }; Ok(command) }
The point here is to convert the message received from the client. We need to do two things:
- Get the command
- Get the command's arguments (if any)
First, we create an iterator to split our vector, so we can separate the command from the arguments:
let mut iter = input.split(|&byte| byte == b' ');
Then, we get the command:
let mut command = iter.next().expect("command in input").to_vec();
At this point, command is a Vec<u8>. To then make the matching easier (because nothing in the RFC of the FTP talks about the fact that commands should be in uppercase or that auth is the same as AUTH or even AuTh), we call the uppercase function, which looks like this:
fn to_uppercase(data: &mut [u8]) { for byte in data { if *byte >= 'a' as u8 && *byte <= 'z' as u8 { *byte -= 32; } } }
Next, we get the arguments by calling next on the iterator iter:
let data = iter.next();
If there are no arguments, no problem! We'll just get None.
Finally, we match the commands:
match command.as_slice() { b"AUTH" => Command::Auth, s => Command::Unknown(str::from_utf8(s).unwrap_or("").to_owned()), }
To do so, we convert our Vec<u8>> into a &[u8] (a slice of u8). To also convert a &str (such as AUTH) into a &[u8], we use the b operator (which is more like saying to the compiler, Hey! Don't worry, just say it's a slice and not a &str!) to allow the matching.
And we're good! We can now write the function to actually read the data from the client:
fn read_all_message(stream: &mut TcpStream) -> Vec<u8> { let buf = &mut [0; 1]; let mut out = Vec::with_capacity(100); loop { match stream.read(buf) { Ok(received) if received > 0 => { if out.is_empty() && buf[0] == b' ' { continue } out.push(buf[0]); } _ => return Vec::new(), } let len = out.len(); if len > 1 && out[len - 2] == b'\r' && out[len - 1] ==
b'\n' { out.pop(); out.pop(); return out; } } }
Here, we read one byte at a time (and it's not a very efficient way to do so; we'll go back on this function later) and return when we get "". We have just added a little security by removing any whitespaces that would come before the command (so as long as we don't have any data in our vector, we won't add any whitespace).
If there is any error, we return an empty vector and stop the reading of the client input.
Like I said earlier, reading byte by byte isn't efficient, but is simpler to demonstrate how it works. So, for now, let's stick to this. This will be done completely differently once the asynchronous programming kicks in.
So, now that we can read and write FTP inputs it's time to actually start the implementation of the commands!
Let's start by creating a new structure:
#[allow(dead_code)] struct Client { cwd: PathBuf, stream: TcpStream, name: Option<String>, }
Here are some quick explanations for the preceding code:
- cwd stands for the current working directory
- stream is the client's socket
- name is the username you got from user authentication (which doesn't really matter, as we won't handle authentication in the first steps)
Now it's time to update the handle_client function:
fn handle_client(mut stream: TcpStream) { println!("new client connected!"); send_cmd(&mut stream, ResultCode::ServiceReadyForNewUser, "Welcome to this FTP
server!"); let client = Client::new(stream); loop { let data = read_all_message(&mut client.stream); if data.is_empty() { println!("client disconnected..."); break; } client.handle_cmd(command::new(data)); } }
When a new client connects to the server, we send them a message to inform them that the server is ready. Then we create a new Client instance, listen on the client socket, and handle its commands. Simple, right?
Two things are missing from this code:
- The Client::new method
- The Client::handle_cmd method
Let's start with the first one:
impl Client { fn new(stream: TcpStream) -> Client { Client { cwd: PathBuf::from("/"), stream: stream, name: None, } } }
Nothing fancy here; the current path is "/" (it corresponds to the root of the server, not to the root of the filesystem!). We have set the client's stream, and the name hasn't been defined yet.
Now let's see the Client::handle_cmd method (needless to say, it'll be the core of this FTP server):
fn handle_cmd(&mut self, cmd: Command) { println!("====> {:?}", cmd); match cmd { Command::Auth => send_cmd(&mut self.stream,
ResultCode::CommandNotImplemented, "Not implemented"), Command::Unknown(s) => send_cmd(&mut self.stream,
ResultCode::UnknownCommand, "Not implemented"), } }
And that's it! Ok, so that's not really it. We still have a lot to add. But my point is, we now only have to add other commands here to make it all work.