First, let's create a new file in src/ called config.rs. To make things easier, we'll use the TOML format for our configuration file. Luckily for us, there is a crate for handling TOML files in Rust, called toml. In addition to this one, we'll use serde to handle serialization and deserialization (very useful!).
Ok, let's start by adding the dependencies into our Cargo.toml file:
toml = "0.4" serde = "1.0" serde_derive = "1.0"
Good, now let's write our Config struct:
pub struct Config { // fields... }
So what should we put in there? The port and address the server should listen on to start, maybe?
pub struct Config { pub server_port: Option<u16>, pub server_addr: Option<String>, }
Done. We also talked about handling authentication. Why not adding it as well? We'll need a new struct for users. Let's call it User (yay for originality!):
pub struct User { pub name: String, pub password: String, }
Now let's add the users into the Config struct:
pub struct Config { pub server_port: Option<u16>, pub server_addr: Option<String>, pub users: Vec<User>, pub admin: Option<User>, }
To make these two struct work with serde, we'll have to add the following tags:
#[derive(Deserialize, Serialize)]
And because we'll need to clone Config, we'll add Debug into the tags, which gives us:
#[derive(Clone, Deserialize, Serialize)] pub struct Config { pub server_port: Option<u16>, pub server_addr: Option<String>, pub admin: Option<User>, pub users: Vec<User>, } #[derive(Clone, Deserialize, Serialize)] pub struct User { pub name: String, pub password: String, }
Ok, we're now ready to implement the reading:
use std::fs::File; use std::path::Path; use std::io::{Read, Write}; use toml; fn get_content<P: AsRef<Path>>(file_path: &P) -> Option<String> { let mut file = File::open(file_path).ok()?; let mut content = String::new(); file.read_to_string(&mut content).ok()?; Some(content) } impl Config { pub fn new<P: AsRef<Path>>(file_path: P) -> Option<Config> { if let Some(content) = get_content(&file_path) { toml::from_str(&content).ok() } else { println!("No config file found so creating a new one in
{}",file_path.as_ref().display()); // In case we didn't find the config file,
we just build a new one. let config = Config { server_port: Some(DEFAULT_PORT), server_addr: Some("127.0.0.1".to_owned()), admin: None, users: vec![User { name: "anonymous".to_owned(), password: "".to_owned(), }], }; let content = toml::to_string(&config).expect("serialization failed"); let mut file = File::create(file_path.as_ref()).expect("couldn't create
file..."); writeln!(file, "{}", content).expect("couldn't fulfill config file..."); Some(config) } } }
Let's go through the Config::new method's code:
if let Some(content) = get_content(&file_path) { toml::from_str(&content).ok() }
Thanks to serde, we can directly load the configuration file from a &str and it'll return our Config struct fully set. Amazing, right?
For information, the get_content function is just a utility function that allows the return of the content of a file, if this file exists.
Also, don't forget to add the DEFAULT_PORT constant:
pub const DEFAULT_PORT: u16 = 1234;
In case the file doesn't exist, we can create a new one with some default values:
else { println!("No config file found so creating a new one in {}", file_path.as_ref().display()); // In case we didn't find the config file, we just build a new one. let config = Config { server_port: Some(DEFAULT_PORT), server_addr: Some("127.0.0.1".to_owned()), admin: None, users: vec![User { name: "anonymous".to_owned(), password: "".to_owned(), }], }; let content = toml::to_string(&config).expect("serialization failed"); let mut file = File::create(file_path.as_ref()).expect("couldn't create
file..."); writeln!(file, "{}", content).expect("couldn't fulfill config file..."); Some(config) }
Now you might wonder, how will we actually be able to generate TOML from our Config struct using this code? With serde's magic once again!
With this, our config file is now complete. Let get back to the main.rs one. First, we'll need to define a new constant:
const CONFIG_FILE: &'static str = "config.toml";
Then, we'll need to update quite a few methods/functions. Let's start with the main function. Add this line at the beginning:
let config = Config::new(CONFIG_FILE).expect("Error while loading config...");
Now pass the config variable to the server function:
if let Err(error) = core.run(server(handle, server_root, config)) {
Next, let's update the server function:
#[async] fn server(handle: Handle, server_root: PathBuf, config: Config) -> io::Result<()> { let port = config.server_port.unwrap_or(DEFAULT_PORT); let addr = SocketAddr::new(IpAddr::V4(config.server_addr.as_ref() .unwrap_or(&"127.0.0.1".to_owned()) .parse() .expect("Invalid IpV4 address...")), port); let listener = TcpListener::bind(&addr, &handle)?; println!("Waiting clients on port {}...", port); #[async] for (stream, addr) in listener.incoming() { let address = format!("[address : {}]", addr); println!("New client: {}", address); handle.spawn(handle_client(stream, handle.clone(), server_root.clone())); handle.spawn(handle_client(stream, handle.clone(), server_root.clone(),
config.clone())); println!("Waiting another client..."); } Ok(()) }
Now, the server is started with the value from the Config struct. However, we still need the user list for each client in order to handle the authentication. To do so, we need to give a Config instance to each Client. In here, to make things simpler, we'll just clone.
Time to update the handle_client function now:
#[async] fn handle_client(stream: TcpStream, handle: Handle, server_root: PathBuf, config: Config) -> result::Result<(), ()> { await!(client(stream, handle, server_root, config)) .map_err(|error| println!("Error handling client: {}", error)) }
Let's update the client function now:
#[async] fn client(stream: TcpStream, handle: Handle, server_root: PathBuf, config: Config) -> Result<()> { let (writer, reader) = stream.framed(FtpCodec).split(); let writer = await!(writer.send(Answer::new(ResultCode::ServiceReadyForNewUser, "Welcome to this FTP server!")))?; let mut client = Client::new(handle, writer, server_root, config); #[async] for cmd in reader { client = await!(client.handle_cmd(cmd))?; } println!("Client closed"); Ok(()) }
The final step is updating the Client struct:
struct Client { cwd: PathBuf, data_port: Option<u16>, data_reader: Option<DataReader>, data_writer: Option<DataWriter>, handle: Handle, name: Option<String>, server_root: PathBuf, transfer_type: TransferType, writer: Writer, is_admin: bool, config: Config, waiting_password: bool, }
The brand new config field seems logical, however what about is_admin and waiting_password? The first one will be used to be able to list/download/overwrite the config.toml file, whereas the second one will be used when the USER command has been used and the server is now expecting the user's password.
Let's add another method to our Client struct:
fn is_logged(&self) -> bool { self.name.is_some() && !self.waiting_password }
Don't forget to update the Config::new method:
fn new(handle: Handle, writer: Writer, server_root: PathBuf, config: Config) -> Client { Client { cwd: PathBuf::from("/"), data_port: None, data_reader: None, data_writer: None, handle, name: None, server_root, transfer_type: TransferType::Ascii, writer, is_admin: false, config, waiting_password: false, } }
Ok, now here comes the huge update! But first, don't forget to add the Pass command:
pub enum Command { // variants... Pass(String), // variants... }
Now the Command::new match:
b"PASS" => Command::Pass(data.and_then(|bytes| String::from_utf8(bytes.to_vec()).map_err(Into::into))?),
Don't forget to also update the AsRef implementation!
Good, we're ready for the last (and very big) step. Let's head to the Client::handle_cmd method:
use config::{DEFAULT_PORT, Config}; use std::path::Path; fn prefix_slash(path: &mut PathBuf) { if !path.is_absolute() { *path = Path::new("/").join(&path); } } #[async] fn handle_cmd(mut self, cmd: Command) -> Result<Self> { println!("Received command: {:?}", cmd); if self.is_logged() { match cmd { Command::Cwd(directory) => return Ok(await!(self.cwd(directory))?), Command::List(path) => return Ok(await!(self.list(path))?), Command::Pasv => return Ok(await!(self.pasv())?), Command::Port(port) => { self.data_port = Some(port); return Ok(await!(self.send(Answer::new(ResultCode::Ok, &format!("Data port is now {}",
port))))?); } Command::Pwd => { let msg = format!("{}", self.cwd.to_str().unwrap_or("")); // small
trick if !msg.is_empty() { let message = format!("\"{}\" ", msg);
return Ok(await!
(self.send(Answer::new(ResultCode::PATHNAMECreated, &message)))?); } else { return Ok(await!(self.send(Answer::new(ResultCode::FileNotFound, "No such file or directory")))?); } } Command::Retr(file) => return Ok(await!(self.retr(file))?), Command::Stor(file) => return Ok(await!(self.stor(file))?), Command::CdUp => { if let Some(path) = self.cwd.parent().map(Path::to_path_buf) { self.cwd = path; prefix_slash(&mut self.cwd); } return Ok(await!(self.send(Answer::new(ResultCode::Ok, "Done")))?); } Command::Mkd(path) => return Ok(await!(self.mkd(path))?), Command::Rmd(path) => return Ok(await!(self.rmd(path))?), _ => (), } } else if self.name.is_some() && self.waiting_password { if let Command::Pass(content) = cmd { let mut ok = false; if self.is_admin { ok = content == self.config.admin.as_ref().unwrap().password; } else { for user in &self.config.users { if Some(&user.name) == self.name.as_ref() { if user.password == content { ok = true; break; } } } } if ok { self.waiting_password = false; let name = self.name.clone().unwrap_or(String::new()); self = await!( self.send(Answer::new(ResultCode::UserLoggedIn, &format!("Welcome {}", name))))?; } else { self = await!(self.send(Answer::new(ResultCode::NotLoggedIn, "Invalid password")))?; } return Ok(self); } } match cmd { Command::Auth => self = await!(self.send(Answer::new(ResultCode::CommandNotImplemented, "Not implemented")))?, Command::Quit => self = await!(self.quit())?, Command::Syst => { self = await!(self.send(Answer::new(ResultCode::Ok, "I won't tell!")))?; } Command::Type(typ) => { self.transfer_type = typ; self = await!(self.send(Answer::new(ResultCode::Ok, "Transfer type changed successfully")))?; } Command::User(content) => { if content.is_empty() { self = await!
(self.send(Answer::new(ResultCode::InvalidParameterOrArgument, "Invalid username")))?; } else { let mut name = None; let mut pass_required = true; self.is_admin = false; if let Some(ref admin) = self.config.admin { if admin.name == content { name = Some(content.clone()); pass_required = admin.password.is_empty() == false; self.is_admin = true; } } // In case the user isn't the admin. if name.is_none() { for user in &self.config.users { if user.name == content { name = Some(content.clone()); pass_required = user.password.is_empty() == false; break; } } } // In case this is an unknown user. if name.is_none() { self = await!(self.send(Answer::new(ResultCode::NotLoggedIn, "Unknown user...")))?; } else { self.name = name.clone(); if pass_required { self.waiting_password = true; self = await!(
self.send(Answer::new(ResultCode::UserNameOkayNeedPassword, &format!("Login OK, password needed for {}", name.unwrap()))))?; } else { self.waiting_password = false; self = await!
(self.send(Answer::new(ResultCode::UserLoggedIn, &format!("Welcome {}!", content))))?; } } } } Command::NoOp => self = await!(self.send(Answer::new(ResultCode::Ok, "Doing nothing")))?, Command::Unknown(s) => self = await!(self.send(Answer::new(ResultCode::UnknownCommand, &format!("\"{}\": Not implemented", s))))?, _ => { // It means that the user tried to send a command while they weren't
logged yet. self = await!(self.send(Answer::new(ResultCode::NotLoggedIn, "Please log first")))?; } } Ok(self) }
I told you it was huge! The main points in here are just the flow rework. The following commands only work when you're logged in:
- Cwd
- List
- Pasv
- Port
- Pwd
- Retr
- Stor
- CdUp
- Mkd
- Rmd
This command only works when you're not yet logged in and the server is waiting for the password:
- Pass
The rest of the commands work in any case. We're almost done in here. Remember when I talked about the security? You wouldn't want anyone to have access to the configuration file with the list of all users, I suppose.