Skip to content

Commit

Permalink
Implement source IP in REST authenticator
Browse files Browse the repository at this point in the history
  • Loading branch information
robklg committed Nov 28, 2023
1 parent 44c6462 commit 74084f6
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 63 deletions.
9 changes: 5 additions & 4 deletions crates/unftp-auth-rest/examples/rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ pub fn main() -> Result<(), Box<dyn std::error::Error>> {
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(())
}
156 changes: 117 additions & 39 deletions crates/unftp-auth-rest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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<DefaultUser> for RestAuthenticator {
#[allow(clippy::type_complexity)]
#[tracing_attributes::instrument]
async fn authenticate(&self, username: &str, creds: &Credentials) -> Result<DefaultUser, AuthenticationError> {
let username_url = utf8_percent_encode(username, NON_ALPHANUMERIC).collect::<String>();
let password = creds.password.as_ref().ok_or(AuthenticationError::BadPassword)?.as_ref();
let password_url = utf8_percent_encode(password, NON_ALPHANUMERIC).collect::<String>();
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::<String>();

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))
Expand All @@ -149,42 +243,26 @@ impl Authenticator<DefaultUser> 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)
}
}
}

/// 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 {
Expand Down
2 changes: 1 addition & 1 deletion crates/unftp-sbe-fs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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
//!
Expand Down
2 changes: 1 addition & 1 deletion crates/unftp-sbe-gcs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions crates/unftp-sbe-gcs/src/response_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
}))
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/auth/anonymous.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down
2 changes: 1 addition & 1 deletion src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/notification/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//!
Expand Down
4 changes: 2 additions & 2 deletions src/storage/mod.rs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
Expand Down
28 changes: 18 additions & 10 deletions src/storage/storage_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,12 @@ pub trait StorageBackend<User: UserDetail>: Send + Sync + Debug {
{
let list = self.list(user, path).await?;

let file_infos: Vec<u8> = list.iter().map(|fi| format!("{}\r\n", fi)).collect::<String>().into_bytes();
let buffer = list.iter().fold(String::new(), |mut buf, fi| {
let _ = write!(buf, "{}\r\n", fi);
buf
});

let file_infos: Vec<u8> = buffer.into_bytes();

Ok(std::io::Cursor::new(file_infos))
}
Expand Down Expand Up @@ -253,15 +258,18 @@ pub trait StorageBackend<User: UserDetail>: 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::<String>()
.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<u8> = 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.
Expand Down

0 comments on commit 74084f6

Please sign in to comment.