From a32ba7209403c486c82af8c0c7408f2c52fc8551 Mon Sep 17 00:00:00 2001 From: veeso Date: Mon, 14 Oct 2024 14:43:00 +0200 Subject: [PATCH 1/3] fix: Passive mode with custom provided TcpStream --- .github/workflows/cargo.yml | 39 ++++-------------- .github/workflows/cli.yml | 7 +--- .github/workflows/coverage.yml | 7 +--- CHANGELOG.md | 7 ++++ Cargo.toml | 2 +- README.md | 2 +- suppaftp/Cargo.toml | 2 +- suppaftp/src/async_ftp/mod.rs | 59 +++++++++++++++++++++++--- suppaftp/src/sync_ftp/mod.rs | 64 ++++++++++++++++++++++------- suppaftp/src/sync_ftp/tls/rustls.rs | 7 ++-- 10 files changed, 129 insertions(+), 67 deletions(-) diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index ca39ff5..fade85e 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: dtolnay/rust-toolchain@stable with: toolchain: stable override: true @@ -18,42 +18,19 @@ jobs: - name: Setup containers run: docker-compose -f "tests/docker-compose.yml" up -d --build - name: Build simple - uses: actions-rs/cargo@v1 - with: - command: build - args: --package suppaftp + run: cargo build --package suppaftp - name: Build secure (native-tls) - uses: actions-rs/cargo@v1 - with: - command: build - args: --features native-tls,deprecated --package suppaftp + run: cargo build --features native-tls,deprecated --package suppaftp - name: Build secure (rustls) - uses: actions-rs/cargo@v1 - with: - command: build - args: --features rustls,deprecated --package suppaftp + run: cargo build --features rustls,deprecated --package suppaftp - name: Build async - uses: actions-rs/cargo@v1 - with: - command: build - args: --features async,deprecated --package suppaftp + run: cargo build --features async,deprecated --package suppaftp - name: Build async-native-tls - uses: actions-rs/cargo@v1 - with: - command: build - args: --features async-native-tls,deprecated --package suppaftp + run: cargo build --features async-native-tls,deprecated --package suppaftp - name: Build all features - uses: actions-rs/cargo@v1 - with: - command: build - args: --features deprecated,native-tls,rustls,async-native-tls,async-rustls --package suppaftp + run: cargo build --features deprecated,native-tls,rustls,async-native-tls,async-rustls --package suppaftp - name: Run tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --lib --package suppaftp --no-default-features --features rustls,native-tls,async-native-tls,async-rustls,with-containers --no-fail-fast - env: - RUST_LOG: trace + run: cargo test --package suppaftp --no-default-features --features rustls,native-tls,async-native-tls,async-rustls,with-containers --no-fail-fast - name: Format run: cargo fmt --all -- --check - name: Clippy diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 471f77c..a6c9718 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -13,16 +13,13 @@ jobs: working-directory: ./suppaftp-cli steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - uses: dtolnay/rust-toolchain@stable with: toolchain: stable override: true components: rustfmt, clippy - name: Build - uses: actions-rs/cargo@v1 - with: - command: build - args: --package suppaftp-cli + run: cargo build --package suppaftp-cli - name: Format run: cargo fmt --all -- --check - name: Clippy diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7cf1ffc..2fc0cd5 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -14,15 +14,12 @@ jobs: - name: Setup containers run: docker-compose -f "tests/docker-compose.yml" up -d --build - name: Setup nightly toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: toolchain: nightly override: true - name: Run tests (nightly) - uses: actions-rs/cargo@v1 - with: - command: test - args: --lib --no-default-features --features native-tls,deprecated,async-native-tls,with-containers --no-fail-fast + run: cargo test --package suppaftp --no-default-features --features native-tls,deprecated,async-native-tls,with-containers --no-fail-fast env: RUST_LOG: trace CARGO_INCREMENTAL: "0" diff --git a/CHANGELOG.md b/CHANGELOG.md index c36e95c..ce83279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog - [Changelog](#changelog) + - [6.0.2](#602) - [6.0.1](#601) - [6.0.0](#600) - [5.4.0](#540) @@ -34,6 +35,12 @@ --- +## 6.0.2 + +Released on 14/10/2024 + +- [Issue 89](https://github.com/veeso/suppaftp/issues/89): added new `FtpStream::passive_stream_builder` to provide a function to build the Passive mode `TcpStream` with a custom builder. This is useful if you need to use some proxy. + ## 6.0.1 Released on 24/05/2024 diff --git a/Cargo.toml b/Cargo.toml index 316a348..54739b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["suppaftp", "suppaftp-cli"] resolver = "2" [workspace.package] -version = "6.0.1" +version = "6.0.2" edition = "2021" authors = [ "Christian Visintin ", diff --git a/README.md b/README.md index 0c0d1cb..ec8b883 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@

Developed by veeso and Matt McCoy

-

Current version: 6.0.1 (24/05/2024)

+

Current version: 6.0.2 (14/10/2024)

Pin> + Send>>; + /// Stream to interface with the FTP server. This interface is only for the command stream. pub struct ImplAsyncFtpStream where @@ -45,6 +53,7 @@ where nat_workaround: bool, welcome_msg: Option, active_timeout: Duration, + passive_stream_builder: Box, #[cfg(not(feature = "async-secure"))] marker: PhantomData, #[cfg(feature = "async-secure")] @@ -85,6 +94,7 @@ where marker: PhantomData {}, mode: Mode::Passive, nat_workaround: false, + passive_stream_builder: Self::default_passive_stream_builder(), welcome_msg: None, #[cfg(feature = "async-secure")] tls_ctx: None, @@ -143,6 +153,7 @@ where reader: BufReader::new(DataStream::Ssl(Box::new(stream))), mode: self.mode, nat_workaround: self.nat_workaround, + passive_stream_builder: self.passive_stream_builder, tls_ctx: Some(Box::new(tls_connector)), domain: Some(String::from(domain)), welcome_msg: self.welcome_msg, @@ -197,6 +208,7 @@ where mode: Mode::Passive, nat_workaround: false, welcome_msg: None, + passive_stream_builder: Self::default_passive_stream_builder(), tls_ctx: None, domain: None, active_timeout: Duration::from_secs(60), @@ -213,6 +225,7 @@ where reader: BufReader::new(DataStream::Ssl(stream.into())), mode: Mode::Passive, nat_workaround: false, + passive_stream_builder: Self::default_passive_stream_builder(), tls_ctx: Some(Box::new(tls_connector)), domain: Some(String::from(domain)), welcome_msg: None, @@ -238,6 +251,18 @@ where self } + /// Set a custom [`StreamBuilder`] for passive mode. + /// + /// The stream builder is a function that takes a `SocketAddr` and returns a `TcpStream` and it's used + /// to create the [`TcpStream`] for the data connection in passive mode. + pub fn passive_stream_builder(mut self, stream_builder: F) -> Self + where + F: Fn(SocketAddr) -> Pin> + Send>> + 'static, + { + self.passive_stream_builder = Box::new(stream_builder); + self + } + /// Returns welcome message retrieved from server (if available) pub fn get_welcome_msg(&self) -> Option<&str> { self.welcome_msg.as_deref() @@ -767,16 +792,12 @@ where Mode::ExtendedPassive => { let addr = self.epsv().await?; self.perform(cmd).await?; - TcpStream::connect(addr) - .await - .map_err(FtpError::ConnectionError)? + (self.passive_stream_builder)(addr).await? } Mode::Passive => { let addr = self.pasv().await?; self.perform(cmd).await?; - TcpStream::connect(addr) - .await - .map_err(FtpError::ConnectionError)? + (self.passive_stream_builder)(addr).await? } }; @@ -1002,6 +1023,16 @@ where self.finalize_retr_stream(data_stream).await?; lines } + + fn default_passive_stream_builder() -> Box { + Box::new(|address| { + Box::pin(async move { + TcpStream::connect(address) + .await + .map_err(FtpError::ConnectionError) + }) + }) + } } #[cfg(test)] @@ -1400,6 +1431,22 @@ mod test { finalize_stream(stream).await; } + #[async_attributes::test] + async fn test_should_set_passive_stream_builder() { + crate::log_init(); + let _ftp_stream = AsyncFtpStream::connect("test.rebex.net:21") + .await + .unwrap() + .passive_stream_builder(|addr| { + Box::pin(async move { + println!("Connecting to {}", addr); + TcpStream::connect(addr) + .await + .map_err(FtpError::ConnectionError) + }) + }); + } + // -- test utils #[cfg(feature = "with-containers")] diff --git a/suppaftp/src/sync_ftp/mod.rs b/suppaftp/src/sync_ftp/mod.rs index 8dca29b..e5ba5fb 100644 --- a/suppaftp/src/sync_ftp/mod.rs +++ b/suppaftp/src/sync_ftp/mod.rs @@ -5,7 +5,6 @@ mod data_stream; mod tls; -use std::fmt::Debug; use std::io::{copy, BufRead, BufReader, Cursor, Read, Write}; #[cfg(not(feature = "secure"))] use std::marker::PhantomData; @@ -32,8 +31,12 @@ use crate::command::Command; use crate::command::ProtectionLevel; use crate::types::Features; +/// A function that creates a new stream for the data connection in passive mode. +/// +/// It takes a [`SocketAddr`] and returns a [`TcpStream`]. +pub type PassiveStreamBuilder = dyn Fn(SocketAddr) -> FtpResult + Send; + /// Stream to interface with the FTP server. This interface is only for the command stream. -#[derive(Debug)] pub struct ImplFtpStream where T: TlsStream, @@ -43,6 +46,7 @@ where nat_workaround: bool, welcome_msg: Option, active_timeout: Duration, + passive_stream_builder: Box, #[cfg(not(feature = "secure"))] marker: PhantomData, #[cfg(feature = "secure")] @@ -80,6 +84,7 @@ where nat_workaround: false, welcome_msg: None, active_timeout: Duration::from_secs(60), + passive_stream_builder: Self::default_passive_stream_builder(), #[cfg(feature = "secure")] tls_ctx: None, #[cfg(feature = "secure")] @@ -106,6 +111,18 @@ where self } + /// Set a custom [`StreamBuilder`] for passive mode. + /// + /// The stream builder is a function that takes a `SocketAddr` and returns a `TcpStream` and it's used + /// to create the [`TcpStream`] for the data connection in passive mode. + pub fn passive_stream_builder(mut self, stream_builder: F) -> Self + where + F: Fn(SocketAddr) -> FtpResult + Send + 'static, + { + self.passive_stream_builder = Box::new(stream_builder); + self + } + /// Set the data channel transfer mode pub fn set_mode(&mut self, mode: Mode) { debug!("Changed mode to {:?}", mode); @@ -153,6 +170,7 @@ where reader: BufReader::new(DataStream::Ssl(Box::new(stream))), mode: self.mode, nat_workaround: self.nat_workaround, + passive_stream_builder: self.passive_stream_builder, tls_ctx: Some(Box::new(tls_connector)), domain: Some(String::from(domain)), welcome_msg: self.welcome_msg, @@ -169,7 +187,7 @@ where /// Connect to remote ftps server using IMPLICIT secure connection. /// - /// > Warning: mind that implicit ftps should be considered deprecated, if you can use explicit mode with `into_secure()` + /// > Warning: mind that implicit ftps should be considered deprecated, if you can use explicit mode with [`ImplFtpStream::into_secure`] /// /// /// ## Example @@ -179,8 +197,8 @@ where /// use suppaftp::native_tls::{TlsConnector, TlsStream}; /// use std::path::Path; /// - /// // Create a TlsConnector - /// // NOTE: For custom options see + /// //Create a TlsConnector + /// //NOTE: For custom options see /// let mut ctx = TlsConnector::new().unwrap(); /// let mut ftp_stream = FtpStream::connect_secure_implicit("127.0.0.1:990", ctx, "localhost").unwrap(); /// ``` @@ -200,6 +218,7 @@ where reader: BufReader::new(DataStream::Tcp(stream)), mode: Mode::Passive, nat_workaround: false, + passive_stream_builder: Self::default_passive_stream_builder(), welcome_msg: None, tls_ctx: None, domain: None, @@ -217,6 +236,7 @@ where mode: Mode::Passive, nat_workaround: false, tls_ctx: Some(Box::new(tls_connector)), + passive_stream_builder: Self::default_passive_stream_builder(), domain: Some(String::from(domain)), welcome_msg: None, active_timeout: Duration::from_secs(60), @@ -239,7 +259,7 @@ where self.welcome_msg.as_deref() } - /// Returns a reference to the underlying TcpStream. + /// Returns a reference to the underlying [`TcpStream`]. /// /// Example: /// ```no_run @@ -439,7 +459,7 @@ where /// This method is a more complicated way to retrieve a file. /// The reader returned should be dropped. /// Also you will have to read the response to make sure it has the correct value. - /// Once file has been read, call `finalize_retr_stream()` + /// Once file has been read, call [`ImplFtpStream::finalize_retr_stream`] pub fn retr_as_stream>(&mut self, file_name: S) -> FtpResult> { debug!("Retrieving '{}'", file_name.as_ref()); let data_stream = self.data_command(Command::Retr(file_name.as_ref().to_string()))?; @@ -447,7 +467,7 @@ where Ok(data_stream) } - /// Finalize retr stream; must be called once the requested file, got previously with `retr_as_stream()` has been read + /// Finalize retr stream; must be called once the requested file, got previously with [`ImplFtpStream::retr_as_stream`] has been read pub fn finalize_retr_stream(&mut self, stream: impl Read) -> FtpResult<()> { debug!("Finalizing retr stream"); // Drop stream NOTE: must be done first, otherwise server won't return any response @@ -475,7 +495,7 @@ where } /// This stores a file on the server. - /// r argument must be any struct which implemenents the Read trait. + /// r argument must be any struct which implemenents the [`Read`] trait. /// Returns amount of written bytes pub fn put_file, R: Read>(&mut self, filename: S, r: &mut R) -> FtpResult { // Get stream @@ -488,7 +508,7 @@ where /// Send PUT command and returns a BufWriter, which references the file created on the server /// The returned stream must be then correctly manipulated to write the content of the source file to the remote destination /// The stream must be then correctly dropped. - /// Once you've finished the write, YOU MUST CALL THIS METHOD: `finalize_put_stream` + /// Once you've finished the write, YOU MUST CALL THIS METHOD: [`ImplFtpStream::finalize_put_stream`] pub fn put_with_stream>(&mut self, filename: S) -> FtpResult> { debug!("Put file {}", filename.as_ref()); let stream = self.data_command(Command::Store(filename.as_ref().to_string()))?; @@ -498,7 +518,7 @@ where /// Finalize put when using stream /// This method must be called once the file has been written and - /// `put_with_stream` has been used to write the file + /// [`ImplFtpStream::put_with_stream`] has been used to write the file pub fn finalize_put_stream(&mut self, stream: impl Write) -> FtpResult<()> { debug!("Finalizing put stream"); // Drop stream NOTE: must be done first, otherwise server won't return any response @@ -510,7 +530,7 @@ where } /// Open specified file for appending data. Returns the stream to append data to specified file. - /// Once you've finished the write, YOU MUST CALL THIS METHOD: `finalize_put_stream` + /// Once you've finished the write, YOU MUST CALL THIS METHOD: [`ImplFtpStream::finalize_put_stream`] pub fn append_with_stream>(&mut self, filename: S) -> FtpResult> { debug!("Appending to file {}", filename.as_ref()); let stream = self.data_command(Command::Appe(filename.as_ref().to_string()))?; @@ -872,11 +892,11 @@ where Mode::ExtendedPassive => self .epsv() .and_then(|addr| self.perform(cmd).map(|_| addr)) - .and_then(|addr| TcpStream::connect(addr).map_err(FtpError::ConnectionError))?, + .and_then(|addr| (self.passive_stream_builder)(addr))?, Mode::Passive => self .pasv() .and_then(|addr| self.perform(cmd).map(|_| addr)) - .and_then(|addr| TcpStream::connect(addr).map_err(FtpError::ConnectionError))?, + .and_then(|addr| (self.passive_stream_builder)(addr))?, }; #[cfg(not(feature = "secure"))] @@ -992,6 +1012,11 @@ where self.finalize_retr_stream(data_stream)?; lines } + + /// Default stream builder + fn default_passive_stream_builder() -> Box { + Box::new(|addr| TcpStream::connect(addr).map_err(FtpError::ConnectionError)) + } } #[cfg(test)] @@ -1461,4 +1486,15 @@ mod test { .with_no_client_auth() } */ + + #[test] + fn test_should_set_passive_stream_builder() { + crate::log_init(); + let _ftp_stream = FtpStream::connect("test.rebex.net:21") + .unwrap() + .passive_stream_builder(|addr| { + println!("Connecting to {}", addr); + TcpStream::connect(addr).map_err(FtpError::ConnectionError) + }); + } } diff --git a/suppaftp/src/sync_ftp/tls/rustls.rs b/suppaftp/src/sync_ftp/tls/rustls.rs index f630371..b21319f 100644 --- a/suppaftp/src/sync_ftp/tls/rustls.rs +++ b/suppaftp/src/sync_ftp/tls/rustls.rs @@ -6,7 +6,8 @@ use std::io::Write; use std::net::TcpStream; use std::sync::Arc; -use rustls::{ClientConfig, ClientConnection, ServerName, StreamOwned}; +use rustls::pki_types::ServerName; +use rustls::{ClientConfig, ClientConnection, StreamOwned}; use super::{TlsConnector, TlsStream}; use crate::{FtpError, FtpResult}; @@ -32,8 +33,8 @@ impl TlsConnector for RustlsConnector { type Stream = RustlsStream; fn connect(&self, domain: &str, stream: TcpStream) -> FtpResult { - let server_name = - ServerName::try_from(domain).map_err(|e| FtpError::SecureError(e.to_string()))?; + let server_name = ServerName::try_from(domain.to_string()) + .map_err(|e| FtpError::SecureError(e.to_string()))?; let connection = ClientConnection::new(Arc::clone(&self.connector), server_name) .map_err(|e| FtpError::SecureError(e.to_string()))?; let stream = StreamOwned::new(connection, stream); From 3c1f4293bba68f3e38fc3e3dbdb65d789fefc911 Mon Sep 17 00:00:00 2001 From: veeso Date: Mon, 14 Oct 2024 14:43:43 +0200 Subject: [PATCH 2/3] fix: ci --- .github/workflows/cargo.yml | 2 +- .github/workflows/coverage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index fade85e..869df3d 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -16,7 +16,7 @@ jobs: override: true components: rustfmt, clippy - name: Setup containers - run: docker-compose -f "tests/docker-compose.yml" up -d --build + run: docker compose -f "tests/docker-compose.yml" up -d --build - name: Build simple run: cargo build --package suppaftp - name: Build secure (native-tls) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 2fc0cd5..34d1153 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Setup containers - run: docker-compose -f "tests/docker-compose.yml" up -d --build + run: docker compose -f "tests/docker-compose.yml" up -d --build - name: Setup nightly toolchain uses: dtolnay/rust-toolchain@stable with: From 10140611cf34812702d6c3cdcb2ccfebc6adb3cd Mon Sep 17 00:00:00 2001 From: veeso Date: Mon, 14 Oct 2024 14:46:26 +0200 Subject: [PATCH 3/3] fix: ci --- .github/workflows/cargo.yml | 2 +- .github/workflows/coverage.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index 869df3d..278eb57 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -30,7 +30,7 @@ jobs: - name: Build all features run: cargo build --features deprecated,native-tls,rustls,async-native-tls,async-rustls --package suppaftp - name: Run tests - run: cargo test --package suppaftp --no-default-features --features rustls,native-tls,async-native-tls,async-rustls,with-containers --no-fail-fast + run: cargo test --lib --package suppaftp --no-default-features --features rustls,native-tls,async-native-tls,async-rustls,with-containers --no-fail-fast - name: Format run: cargo fmt --all -- --check - name: Clippy diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 34d1153..d0501b8 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,7 +19,7 @@ jobs: toolchain: nightly override: true - name: Run tests (nightly) - run: cargo test --package suppaftp --no-default-features --features native-tls,deprecated,async-native-tls,with-containers --no-fail-fast + run: cargo test --lib --package suppaftp --no-default-features --features native-tls,deprecated,async-native-tls,with-containers --no-fail-fast env: RUST_LOG: trace CARGO_INCREMENTAL: "0"