diff --git a/crates/unftp-auth-rest/examples/rest.rs b/crates/unftp-auth-rest/examples/rest.rs index 2167e778..f52513ed 100644 --- a/crates/unftp-auth-rest/examples/rest.rs +++ b/crates/unftp-auth-rest/examples/rest.rs @@ -12,18 +12,19 @@ pub fn main() -> Result<(), Box> { let authenticator: RestAuthenticator = Builder::new() .with_username_placeholder("{USER}".to_string()) .with_password_placeholder("{PASS}".to_string()) - .with_url("https://authenticateme.mydomain.com/path".to_string()) + .with_source_ip_placeholder("{IP}".to_string()) + .with_url("http://127.0.0.1:5000/authenticate".to_string()) .with_method(hyper::Method::POST) - .with_body(r#"{"username":"{USER}","password":"{PASS}"}"#.to_string()) + .with_body(r#"{"username":"{USER}","password":"{PASS}", "source_ip":"{IP}"}"#.to_string()) .with_selector("/status".to_string()) - .with_regex("pass".to_string()) + .with_regex("success".to_string()) .build()?; let addr = "127.0.0.1:2121"; let server = libunftp::Server::with_fs(std::env::temp_dir()).authenticator(Arc::new(authenticator)); println!("Starting ftp server on {}", addr); - let runtime = TokioBuilder::new_current_thread().build()?; + let runtime = TokioBuilder::new_current_thread().enable_io().enable_time().build()?; runtime.block_on(server.listen(addr))?; Ok(()) } diff --git a/crates/unftp-auth-rest/src/lib.rs b/crates/unftp-auth-rest/src/lib.rs index c4fb7565..d0329409 100644 --- a/crates/unftp-auth-rest/src/lib.rs +++ b/crates/unftp-auth-rest/src/lib.rs @@ -22,6 +22,7 @@ use std::string::String; pub struct RestAuthenticator { username_placeholder: String, password_placeholder: String, + source_ip_placeholder: String, method: Method, url: String, @@ -30,11 +31,12 @@ pub struct RestAuthenticator { regex: Regex, } -/// Used to build the [`RestAuthenticator`](crate::RestAuthenticator) +/// Used to build the [`RestAuthenticator`] #[derive(Clone, Debug, Default)] pub struct Builder { username_placeholder: String, password_placeholder: String, + source_ip_placeholder: String, method: Method, url: String, @@ -44,23 +46,105 @@ pub struct Builder { } impl Builder { + /// Creates a new `Builder` instance with default settings. /// + /// This method initializes a new builder that you can use to configure and + /// ultimately construct a [`RestAuthenticator`]. Each setting has a default + /// value that can be customized through the builder's methods. + /// + /// For customization we have several methods: + /// The placeholder methods (E.g.: `with_username_placeholder`) allow you to + /// configure placeholders for certain fields. + /// These placeholders, will be replaced by actual values (FTP username, + /// password, or the client's source IP) when preparing requests. + /// You can use these placeholders in the templates supplied `with_url` or + /// `with_body` . + /// + /// + pub fn new() -> Builder { Builder { ..Default::default() } } - /// Specifies the placeholder string in the rest of the fields that would be replaced by the username + /// Sets the placeholder for the FTP username. + /// + /// This placeholder will be replaced with the actual FTP username in the fields where it's used. + /// Refer to the general placeholder concept above for more information. + /// + /// # Arguments + /// + /// * `s` - A `String` representing the placeholder for the FTP username. + /// + /// # Examples + /// + /// ``` + /// # use unftp_auth_rest::{Builder, RestAuthenticator}; + /// # + /// let mut builder = Builder::new() + /// .with_username_placeholder("{USER}".to_string()) + /// .with_body(r#"{"username":"{USER}","password":"{PASS}"}"#.to_string()); + /// ``` + /// + /// In the example above, `"{USER}"` within the body template is replaced with the actual FTP username during request + /// preparation. If the placeholder configuration is not set, any `"{USER}"` text would stay unreplaced in the request. pub fn with_username_placeholder(mut self, s: String) -> Self { self.username_placeholder = s; self } - /// specify the placeholder string in the rest of the fields that would be replaced by the password + /// Sets the placeholder for the FTP password. + /// + /// This placeholder will be replaced with the actual FTP password in the fields where it's used. + /// Refer to the general placeholder concept above for more information. + /// + /// # Arguments + /// + /// * `s` - A `String` representing the placeholder for the FTP password. + /// + /// # Examples + /// + /// ``` + /// # use unftp_auth_rest::{Builder, RestAuthenticator}; + /// # + /// let mut builder = Builder::new() + /// .with_password_placeholder("{PASS}".to_string()) + /// .with_body(r#"{"username":"{USER}","password":"{PASS}"}"#.to_string()); + /// ``` + /// + /// In the example above, "{PASS}" within the body template is replaced with the actual FTP password during request + /// preparation. If the placeholder configuration is not set, any "{PASS}" text would stay unreplaced in the request. pub fn with_password_placeholder(mut self, s: String) -> Self { self.password_placeholder = s; self } + /// Sets the placeholder for the source IP of the FTP client. + /// + /// This placeholder will be replaced with the actual source IP in the fields where it's used. + /// Refer to the general placeholder concept above for more information. + /// + /// # Arguments + /// + /// * `s` - A `String` representing the placeholder for the FTP client's source IP. + /// + /// # Examples + /// + /// ``` + /// # use unftp_auth_rest::{Builder, RestAuthenticator}; + /// # + /// let mut builder = Builder::new() + /// .with_source_ip_placeholder("{IP}".to_string()) + /// .with_body(r#"{"username":"{USER}","password":"{PASS}", "source_ip":"{IP}"}"#.to_string()); + /// ``` + /// + /// In the example above, "{IP}" within the body template is replaced with the actual source IP of the FTP client + /// during request preparation. If the placeholder configuration is not set, any "{IP}" text would stay unreplaced + /// in the request. + pub fn with_source_ip_placeholder(mut self, s: String) -> Self { + self.source_ip_placeholder = s; + self + } + /// specify HTTP method pub fn with_method(mut self, s: Method) -> Self { self.method = s; @@ -97,6 +181,7 @@ impl Builder { Ok(RestAuthenticator { username_placeholder: self.username_placeholder, password_placeholder: self.password_placeholder, + source_ip_placeholder: self.source_ip_placeholder, method: self.method, url: self.url, body: self.body, @@ -107,37 +192,46 @@ impl Builder { } impl RestAuthenticator { - fn fill_encoded_placeholders(&self, string: &str, username: &str, password: &str) -> String { - string - .replace(&self.username_placeholder, username) - .replace(&self.password_placeholder, password) + fn fill_encoded_placeholders(&self, string: &str, username: &str, password: &str, source_ip: &str) -> String { + let mut result = string.to_owned(); + + if !self.username_placeholder.is_empty() { + result = result.replace(&self.username_placeholder, username); + } + if !self.password_placeholder.is_empty() { + result = result.replace(&self.password_placeholder, password); + } + if !self.source_ip_placeholder.is_empty() { + result = result.replace(&self.source_ip_placeholder, source_ip); + } + + result } } -// FIXME: add support for authenticated user #[async_trait] impl Authenticator for RestAuthenticator { - #[allow(clippy::type_complexity)] #[tracing_attributes::instrument] async fn authenticate(&self, username: &str, creds: &Credentials) -> Result { let username_url = utf8_percent_encode(username, NON_ALPHANUMERIC).collect::(); let password = creds.password.as_ref().ok_or(AuthenticationError::BadPassword)?.as_ref(); let password_url = utf8_percent_encode(password, NON_ALPHANUMERIC).collect::(); - let url = self.fill_encoded_placeholders(&self.url, &username_url, &password_url); + let source_ip = creds.source_ip.to_string(); + let source_ip_url = utf8_percent_encode(&source_ip, NON_ALPHANUMERIC).collect::(); - let username_json = encode_string_json(username); - let password_json = encode_string_json(password); - let body = self.fill_encoded_placeholders(&self.body, &username_json, &password_json); + let url = self.fill_encoded_placeholders(&self.url, &username_url, &password_url, &source_ip_url); - // FIXME: need to clone too much, just to keep tokio::spawn() happy, with its 'static requirement. is there a way maybe to work this around with proper lifetime specifiers? Or is it better to just clone the whole object? - let method = self.method.clone(); - let selector = self.selector.clone(); - let regex = self.regex.clone(); + let username_json = serde_json::to_string(username).map_err(|e| AuthenticationError::ImplPropagated(e.to_string(), None))?; + let trimmed_username_json = username_json.trim_matches('"'); + let password_json = serde_json::to_string(password).map_err(|e| AuthenticationError::ImplPropagated(e.to_string(), None))?; + let trimmed_password_json = password_json.trim_matches('"'); + let source_ip_json = serde_json::to_string(&source_ip).map_err(|e| AuthenticationError::ImplPropagated(e.to_string(), None))?; + let trimmed_source_ip_json = source_ip_json.trim_matches('"'); - //slog::debug!("{} {}", url, body); + let body = self.fill_encoded_placeholders(&self.body, trimmed_username_json, trimmed_password_json, trimmed_source_ip_json); let req = Request::builder() - .method(method) + .method(&self.method) .header("Content-type", "application/json") .uri(url) .body(Body::from(body)) @@ -149,17 +243,19 @@ impl Authenticator for RestAuthenticator { .request(req) .await .map_err(|e| AuthenticationError::with_source("rest authenticator http client error", e))?; + let body_bytes = hyper::body::to_bytes(resp.into_body()) .await .map_err(|e| AuthenticationError::with_source("rest authenticator http client error", e))?; let body: Value = serde_json::from_slice(&body_bytes).map_err(|e| AuthenticationError::with_source("rest authenticator unmarshalling error", e))?; - let parsed = match body.pointer(&selector) { + let parsed = match body.pointer(&self.selector) { Some(parsed) => parsed.to_string(), None => json!(null).to_string(), }; - if regex.is_match(&parsed) { + println!("{}", parsed); + if self.regex.is_match(&parsed) { Ok(DefaultUser {}) } else { Err(AuthenticationError::BadPassword) @@ -167,24 +263,6 @@ impl Authenticator for RestAuthenticator { } } -/// limited capabilities, meant for us-ascii username and password only, really -fn encode_string_json(string: &str) -> String { - let mut res = String::with_capacity(string.len() * 2); - - for i in string.chars() { - match i { - '\\' => res.push_str("\\\\"), - '"' => res.push_str("\\\""), - ' '..='~' => res.push(i), - _ => { - //slog::error!("special character {} is not supported", i); - } - } - } - - res -} - /// Possible errors while doing REST lookup #[derive(Debug)] pub enum RestError { diff --git a/crates/unftp-sbe-fs/src/lib.rs b/crates/unftp-sbe-fs/src/lib.rs index fbfc0b66..93774590 100644 --- a/crates/unftp-sbe-fs/src/lib.rs +++ b/crates/unftp-sbe-fs/src/lib.rs @@ -1,4 +1,4 @@ -//! A libunftp [`StorageBackend`](libunftp::storage::StorageBackend) that uses a local filesystem, like a traditional FTP server. +//! A libunftp [`StorageBackend`] that uses a local filesystem, like a traditional FTP server. //! //! Here is an example for using this storage backend //! diff --git a/crates/unftp-sbe-gcs/src/lib.rs b/crates/unftp-sbe-gcs/src/lib.rs index b0eaf740..2eb486e0 100644 --- a/crates/unftp-sbe-gcs/src/lib.rs +++ b/crates/unftp-sbe-gcs/src/lib.rs @@ -79,7 +79,7 @@ use std::{ path::{Path, PathBuf}, }; -/// A [`StorageBackend`](libunftp::storage::StorageBackend) that uses Cloud storage from Google. +/// A [`StorageBackend`] that uses Cloud storage from Google. /// cloned for each controlchan! #[derive(Clone, Debug)] pub struct CloudStorage { diff --git a/crates/unftp-sbe-gcs/src/response_body.rs b/crates/unftp-sbe-gcs/src/response_body.rs index 20e13e1f..590d9535 100644 --- a/crates/unftp-sbe-gcs/src/response_body.rs +++ b/crates/unftp-sbe-gcs/src/response_body.rs @@ -3,7 +3,7 @@ use base64::Engine; use chrono::prelude::*; use libunftp::storage::{Error, ErrorKind, Fileinfo}; use serde::{de, Deserialize}; -use std::fmt::Display; +use std::fmt::{Display, Write}; use std::str::FromStr; use std::time::SystemTime; use std::{iter::Extend, path::PathBuf}; @@ -132,7 +132,10 @@ impl Item { let md5 = base64::engine::general_purpose::STANDARD .decode(&self.md5_hash) .map_err(|e| Error::new(ErrorKind::LocalError, e))?; - Ok(md5.iter().map(|b| format!("{:02x}", b)).collect()) + Ok(md5.iter().fold(String::new(), |mut output, b| { + let _ = write!(output, "{b:02x}"); + output + })) } } diff --git a/src/auth/anonymous.rs b/src/auth/anonymous.rs index 7ce38551..1b40dbf5 100644 --- a/src/auth/anonymous.rs +++ b/src/auth/anonymous.rs @@ -4,7 +4,7 @@ use crate::auth::*; use async_trait::async_trait; /// -/// [`Authenticator`](crate::auth::Authenticator) implementation that simply allows everyone. +/// [`Authenticator`] implementation that simply allows everyone. /// /// # Example /// diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 2f3af055..10419df6 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,6 +1,6 @@ #![deny(missing_docs)] -//! Contains the [`Authenticator`](crate::auth::Authenticator) and [`UserDetail`](crate::auth::UserDetail) +//! Contains the [`Authenticator`] and [`UserDetail`] //! traits that are used to extend libunftp's authentication and user detail storage capabilities. //! //! Pre-made implementations exists on crates.io (search for `unftp-auth-`) and you can define your diff --git a/src/notification/mod.rs b/src/notification/mod.rs index 75182c27..780a3be0 100644 --- a/src/notification/mod.rs +++ b/src/notification/mod.rs @@ -2,11 +2,11 @@ //! //! Allows users to listen to events emitted by libunftp. //! -//! To listen for changes in data implement the [`DataListener`](crate::notification::DataListener) +//! To listen for changes in data implement the [`DataListener`] //! trait and use the [`Server::notify_data`](crate::Server::notify_data) method //! to make libunftp notify it. //! -//! To listen to logins and logouts implement the [`PresenceListener`](crate::notification::PresenceListener) +//! To listen to logins and logouts implement the [`PresenceListener`] //! trait and use the [`Server::notify_presence`](crate::Server::notify_data) method //! to make libunftp use it. //! diff --git a/src/storage/mod.rs b/src/storage/mod.rs index bccc2e8d..5d33f266 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,4 +1,4 @@ -//! Contains the [`StorageBackend`](crate::storage::StorageBackend) trait that can be implemented to +//! Contains the [`StorageBackend`] trait that can be implemented to //! create virtual file systems for libunftp. //! //! Pre-made implementations exists on crates.io (search for `unftp-sbe-`) and you can define your @@ -12,7 +12,7 @@ //! async-trait = "0.1.50" //! ``` //! -//! 2. Implement the [`StorageBackend`](crate::storage::StorageBackend) trait and optionally the [`Metadata`](crate::storage::Metadata) trait: +//! 2. Implement the [`StorageBackend`] trait and optionally the [`Metadata`] trait: //! //! ```no_run //! use async_trait::async_trait; diff --git a/src/storage/storage_backend.rs b/src/storage/storage_backend.rs index fcc1dc6f..1593eb86 100644 --- a/src/storage/storage_backend.rs +++ b/src/storage/storage_backend.rs @@ -224,7 +224,12 @@ pub trait StorageBackend: Send + Sync + Debug { { let list = self.list(user, path).await?; - let file_infos: Vec = list.iter().map(|fi| format!("{}\r\n", fi)).collect::().into_bytes(); + let buffer = list.iter().fold(String::new(), |mut buf, fi| { + let _ = write!(buf, "{}\r\n", fi); + buf + }); + + let file_infos: Vec = buffer.into_bytes(); Ok(std::io::Cursor::new(file_infos)) } @@ -253,15 +258,18 @@ pub trait StorageBackend: Send + Sync + Debug { { let list = self.list(user, path).await.map_err(|_| std::io::Error::from(std::io::ErrorKind::Other))?; - let bytes = list - .iter() - .map(|file| { - let info = file.path.file_name().unwrap_or_else(|| std::ffi::OsStr::new("")).to_str().unwrap_or(""); - format!("{}\r\n", info) - }) - .collect::() - .into_bytes(); - Ok(std::io::Cursor::new(bytes)) + let buffer = list.iter().fold(String::new(), |mut buf, fi| { + let _ = write!( + buf, + "{}\r\n", + fi.path.file_name().unwrap_or_else(|| std::ffi::OsStr::new("")).to_str().unwrap_or("") + ); + buf + }); + + let file_infos: Vec = buffer.into_bytes(); + + Ok(std::io::Cursor::new(file_infos)) } /// Gets the content of the given FTP file from offset start_pos file by copying it to the output writer.