diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml
index ca39ff5..2033860 100644
--- a/.github/workflows/cargo.yml
+++ b/.github/workflows/cargo.yml
@@ -10,50 +10,24 @@ 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
components: rustfmt, clippy
- - 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 --lib --package suppaftp --no-default-features --features rustls,native-tls,async-native-tls,async-rustls --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..8187191 100644
--- a/.github/workflows/cli.yml
+++ b/.github/workflows/cli.yml
@@ -13,16 +13,12 @@ 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..5081b8e 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -1,4 +1,4 @@
-name: Coverage
+name: coverage
on: [push, pull_request]
@@ -6,33 +6,25 @@ env:
CARGO_TERM_COLOR: always
jobs:
- build:
+ coverage:
+ name: Generate coverage
runs-on: ubuntu-latest
-
steps:
- - uses: actions/checkout@v2
- - name: Setup containers
- run: docker-compose -f "tests/docker-compose.yml" up -d --build
- - name: Setup nightly toolchain
- uses: actions-rs/toolchain@v1
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ - name: Setup rust toolchain
+ uses: dtolnay/rust-toolchain@stable
with:
- toolchain: nightly
- override: true
- - name: Run tests (nightly)
- uses: actions-rs/cargo@v1
+ toolchain: stable
+ - uses: taiki-e/install-action@v2
with:
- command: test
- args: --lib --no-default-features --features native-tls,deprecated,async-native-tls,with-containers --no-fail-fast
- env:
- RUST_LOG: trace
- CARGO_INCREMENTAL: "0"
- RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
- RUSTDOCFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
- - name: Coverage with grcov
- id: coverage
- uses: actions-rs/grcov@v0.1
+ tool: cargo-llvm-cov
+ - name: Run tests
+ run: cargo llvm-cov --no-fail-fast --no-default-features --features native-tls,deprecated,async-native-tls --workspace --lcov --output-path lcov.info
- name: Coveralls
- uses: coverallsapp/github-action@v1.1.1
+ uses: coverallsapp/github-action@v2.3.6
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- path-to-lcov: ${{ steps.coverage.outputs.report }}
+ file: lcov.info
+ # currently we only run one coverage report per build
+ parallel: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c36e95c..5323e12 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,14 @@
# Changelog
- [Changelog](#changelog)
+ - [6.1.1](#611)
+ - [6.1.0](#610)
+ - [6.0.7](#607)
+ - [6.0.6](#606)
+ - [6.0.5](#605)
+ - [6.0.4](#604)
+ - [6.0.3](#603)
+ - [6.0.2](#602)
- [6.0.1](#601)
- [6.0.0](#600)
- [5.4.0](#540)
@@ -34,6 +42,55 @@
---
+## 6.1.1
+
+Released on 17/03/2025
+
+- added a couple of logs to debug streams.
+
+## 6.1.0
+
+Released on 10/03/2025
+
+- [Issue 100](https://github.com/veeso/suppaftp/issues/100): Migrated away from unmaintained `async-tls` to `futures-rustls`
+- [Issue 98](https://github.com/veeso/suppaftp/issues/98): doc: fixed minor typos that referenced `termscp`
+
+## 6.0.7
+
+- [Issue 88](https://github.com/veeso/suppaftp/issues/88): Removed `ip.is_private()` check on NAT workaround, which prevented public IPs to be used for Natting.
+
+## 6.0.6
+
+Released on 17/01/2025
+
+- [Issue 95](https://github.com/veeso/suppaftp/issues/95): Fixed TLS Stream not properly closed when using rustls.
+
+## 6.0.5
+
+Released on 27/11/2024
+
+- [Force rustls to use ring](https://github.com/veeso/suppaftp/issues/94)
+
+## 6.0.4
+
+Released on 26/10/2024
+
+- Added `Sync` to client.
+- Added unit test to guarantee that sync FtpStream stays `Sync`
+
+## 6.0.3
+
+Released on 15/10/2024
+
+- Added `Send` marker to the Closure: `dyn Fn(SocketAddr) -> Pin> + Send>> + Send;`
+- Added unit test to guarantee that FtpStream stays `Send`
+
+## 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/CONTRIBUTING.md b/CONTRIBUTING.md
index 1342ab7..e80d1c2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -37,7 +37,7 @@ Check the issue is always assigned to `veeso`.
### Bug reports
-If you want to report an issue or a bug you've encountered while using termscp, open an issue using the `Bug report` template.
+If you want to report an issue or a bug you've encountered while using suppaftp, open an issue using the `Bug report` template.
The `Bug` label should already be set and the issue should already be assigned to `veeso`.
When you open a bug try to be the most precise as possible in describing your issue. I'm not saying you should always be that precise, since sometimes it's very easy for maintainers to understand what you're talking about. Just try to be reasonable to understand sometimes we might not know what you're talking about or we just don't have the technical knowledge you might think.
@@ -53,7 +53,7 @@ Maintainers will may add additional labels to your issue:
### Feature requests
Whenever you have a good idea which chould improve the project, it is a good idea to submit it to the project owner.
-The first thing you should do though, is not starting to write the code, but is to become concern about how termscp works, what kind
+The first thing you should do though, is not starting to write the code, but is to become concern about how suppaftp works, what kind
of contribution I appreciate and what kind of contribution I won't consider.
Said so, follow these steps:
@@ -101,7 +101,7 @@ Let's make it simple and clear:
In addition to the process described for the PRs, I've also decided to introduce a list of guidelines to follow when writing the code, that should be followed:
-1. **Let's stop the NPM apocalypse**: personally I'm against the abuse of dependencies we make in software projects and I think that NodeJS has opened the way to this drama (and has already gone too far). Nowadays nobody cares about adding hundreds of dependencies to their projects. Don't misunderstand me: I think that package managers are cool, but I'm totally against the abuse we're making of them. I think when we work on a project, we should try to use the minor quantity of dependencies as possible, especially because it's not hard to see how many libraries are getting abandoned right now, causing compatibility issues after a while. So please, when working on termscp, try not to add useless dependencies.
+1. **Let's stop the NPM apocalypse**: personally I'm against the abuse of dependencies we make in software projects and I think that NodeJS has opened the way to this drama (and has already gone too far). Nowadays nobody cares about adding hundreds of dependencies to their projects. Don't misunderstand me: I think that package managers are cool, but I'm totally against the abuse we're making of them. I think when we work on a project, we should try to use the minor quantity of dependencies as possible, especially because it's not hard to see how many libraries are getting abandoned right now, causing compatibility issues after a while. So please, when working on suppaftp, try not to add useless dependencies.
2. **No C-bindings**: personally I think that Rust still relies too much on C. And that's bad, really bad. Many libraries in Rust are just wrappers to C libraries, which is a huge problem, especially considering this is a multiplatform project. Everytime you add a C-binding to your project, you're forcing your users to install additional libraries to their systems. Sometimes these libraries are already installed on their systems (as happens for libssh2 or openssl in this case), but sometimes not. So if you really have to add a dependency to this project, please AVOID completely adding C-bounded libraries.
3. **Test units matter**: Whenever you implement something new to this project, always implement test units which cover the most cases as possible.
4. **Comments are useful**: Many people say that the code should be that simple to talk by itself about what it does, and comments should then be useless. I personally don't agree. I'm not saying they're wrong, but I'm just saying that this approach has, in my personal opinion, many aspects which are underrated:
diff --git a/Cargo.toml b/Cargo.toml
index 316a348..b50c712 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,8 +3,9 @@ members = ["suppaftp", "suppaftp-cli"]
resolver = "2"
[workspace.package]
-version = "6.0.1"
+version = "6.1.1"
edition = "2021"
+rust-version = "1.71.1"
authors = [
"Christian Visintin ",
"Matt McCoy ",
diff --git a/README.md b/README.md
index 0c0d1cb..d91ef00 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.1.1 (17/03/2025)
💡 If you don't know what to choose, `native-tls` should be preferred for compatibility reasons.
+> [!NOTE]
+> 💡 If you don't know what to choose, `native-tls` should be preferred for compatibility reasons.
> ❗ If you want to link libssl statically, enable feature `native-tls-vendored`
#### Async support
@@ -148,8 +149,9 @@ If you want to enable **async** support, you must enable `async` feature in your
suppaftp = { version = "^6", features = ["async"] }
```
-> ⚠️ If you want to enable both **native-tls** and **async** you must use the **async-native-tls** feature ⚠️
-> ⚠️ If you want to enable both **rustls** and **async** you must use the **async-rustls** feature ⚠️
+> [!CAUTION]
+> ⚠️ If you want to enable both **native-tls** and **async** you must use the **async-native-tls** feature ⚠️
+> ⚠️ If you want to enable both **rustls** and **async** you must use the **async-rustls** feature ⚠️
> ❗ If you want to link libssl statically, enable feature `async-native-tls-vendored`
#### Deprecated methods
diff --git a/suppaftp-cli/Cargo.toml b/suppaftp-cli/Cargo.toml
index ec5b4af..315357d 100644
--- a/suppaftp-cli/Cargo.toml
+++ b/suppaftp-cli/Cargo.toml
@@ -2,6 +2,7 @@
version = { workspace = true }
authors = { workspace = true }
edition = { workspace = true }
+rust-version = { workspace = true }
license = { workspace = true }
keywords = { workspace = true }
categories = { workspace = true }
diff --git a/suppaftp/Cargo.toml b/suppaftp/Cargo.toml
index 5a08fc3..130859c 100644
--- a/suppaftp/Cargo.toml
+++ b/suppaftp/Cargo.toml
@@ -3,6 +3,7 @@ name = "suppaftp"
version = { workspace = true }
authors = { workspace = true }
edition = { workspace = true }
+rust-version = { workspace = true }
license = { workspace = true }
keywords = { workspace = true }
categories = { workspace = true }
@@ -27,17 +28,23 @@ path = "src/lib.rs"
chrono = { version = "^0.4", default-features = false, features = ["clock"] }
lazy-regex = "^3"
log = "^0.4"
-thiserror = "^1"
+thiserror = "^2"
# async
async-std = { version = "^1.10", optional = true }
async-native-tls-crate = { package = "async-native-tls", version = "^0.5", optional = true }
async-trait = { version = "0.1.64", optional = true }
-async-tls = { version = "^0.13", optional = true }
+futures-rustls = { version = "^0.26", optional = true }
pin-project = { version = "^1", optional = true }
+rustls-pki-types = { version = "1", optional = true, features = ["alloc"] }
# secure
native-tls-crate = { package = "native-tls", version = "^0.2", optional = true }
-rustls-crate = { package = "rustls", version = "^0.21", optional = true }
-futures-lite = "2.0.0"
+rustls-crate = { package = "rustls", version = "^0.23", default-features = false, features = [
+ "logging",
+ "ring",
+ "std",
+ "tls12",
+], optional = true }
+futures-lite = "2"
[dev-dependencies]
async-attributes = "1.1.2"
@@ -45,15 +52,16 @@ env_logger = "^0.11"
pretty_assertions = "^1.0.0"
rand = "^0.8.4"
serial_test = "^3.0"
+testcontainers = { version = "0.23", features = ["blocking"] }
webpki-roots = "0.26"
[features]
default = []
# Enable async support for suppaftp
-async = ["async-std", "async-trait", "pin-project"]
+async = ["dep:async-std", "dep:async-trait", "dep:pin-project"]
async-default-tls = ["async-native-tls"]
-async-native-tls = ["async-native-tls-crate", "async-secure"]
-async-rustls = ["async-tls", "async-secure"]
+async-native-tls = ["dep:async-native-tls-crate", "async-secure"]
+async-rustls = ["dep:futures-rustls", "dep:rustls-pki-types", "async-secure"]
async-secure = ["async"]
# Enable deprecated FTP/FTPS methods
@@ -73,8 +81,6 @@ secure = []
# Disable logging
no-log = ["log/max_level_off"]
-# Must be enabled whenever testing with docker containers
-with-containers = []
[package.metadata.docs.rs]
all-features = true
diff --git a/suppaftp/src/async_ftp/mod.rs b/suppaftp/src/async_ftp/mod.rs
index bf305e5..07bbadd 100644
--- a/suppaftp/src/async_ftp/mod.rs
+++ b/suppaftp/src/async_ftp/mod.rs
@@ -5,9 +5,11 @@
mod data_stream;
mod tls;
+use std::future::Future;
#[cfg(not(feature = "async-secure"))]
use std::marker::PhantomData;
-use std::net::{Ipv4Addr, SocketAddr};
+use std::net::SocketAddr;
+use std::pin::Pin;
use std::string::String;
use std::time::Duration;
@@ -27,13 +29,21 @@ pub use tls::{AsyncNativeTlsConnector, AsyncNativeTlsStream};
#[cfg(feature = "async-rustls")]
pub use tls::{AsyncRustlsConnector, AsyncRustlsStream};
-use super::regex::{EPSV_PORT_RE, MDTM_RE, PASV_PORT_RE, SIZE_RE};
+use super::regex::{EPSV_PORT_RE, MDTM_RE, SIZE_RE};
use super::types::{FileType, FtpError, FtpResult, Mode, Response};
use super::Status;
use crate::command::Command;
#[cfg(feature = "async-secure")]
use crate::command::ProtectionLevel;
use crate::types::Features;
+use crate::FtpStream;
+
+/// 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) -> Pin> + Send + Sync>>
+ + Send
+ + Sync;
/// Stream to interface with the FTP server. This interface is only for the command stream.
pub struct ImplAsyncFtpStream
@@ -45,6 +55,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 +96,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 +155,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 +210,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 +227,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 +253,21 @@ 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 + Sync>>
+ + Send
+ + Sync
+ + '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 +797,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?
}
};
@@ -825,33 +851,16 @@ where
self.perform(Command::Pasv).await?;
// PASV response format : 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
let response: Response = self.read_response(Status::PassiveMode).await?;
- let response_str = response.as_string().map_err(|_| FtpError::BadResponse)?;
- let caps = PASV_PORT_RE
- .captures(&response_str)
- .ok_or_else(|| FtpError::UnexpectedResponse(response.clone()))?;
- // If the regex matches we can be sure groups contains numbers
- let (oct1, oct2, oct3, oct4) = (
- caps[1].parse::().unwrap(),
- caps[2].parse::().unwrap(),
- caps[3].parse::().unwrap(),
- caps[4].parse::().unwrap(),
- );
- let (msb, lsb) = (
- caps[5].parse::().unwrap(),
- caps[6].parse::().unwrap(),
- );
- let ip = Ipv4Addr::new(oct1, oct2, oct3, oct4);
- let port = (u16::from(msb) << 8) | u16::from(lsb);
- let addr = SocketAddr::new(ip.into(), port);
+ let addr = FtpStream::parse_passive_address_from_response(response)?;
trace!("Passive address: {}", addr);
- if self.nat_workaround && ip.is_private() {
+ if self.nat_workaround {
let mut remote = self
.reader
.get_ref()
.get_ref()
.peer_addr()
.map_err(FtpError::ConnectionError)?;
- remote.set_port(port);
+ remote.set_port(addr.port());
trace!("Replacing site local address {} with {}", addr, remote);
Ok(remote)
} else {
@@ -897,6 +906,7 @@ where
match data_stream.read_line(&mut line).await {
Ok(0) => break,
Ok(_) => {
+ trace!("STREAM IN: {:?}", line);
if line.ends_with('\n') {
line.pop();
if line.ends_with('\r') {
@@ -908,7 +918,10 @@ where
}
lines.push(line);
}
- Err(_) => return Err(FtpError::BadResponse),
+ Err(err) => {
+ error!("failed to get lines from stream: {err}");
+ return Err(FtpError::BadResponse);
+ }
}
}
trace!("Lines from stream {:?}", lines);
@@ -1002,133 +1015,41 @@ 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)]
mod test {
- #[cfg(feature = "async-native-tls")]
- use async_native_tls::TlsConnector as NativeTlsConnector;
- #[cfg(any(feature = "with-containers", feature = "async-secure"))]
+ use std::sync::Arc;
+
+ #[cfg(feature = "async-secure")]
use pretty_assertions::assert_eq;
- #[cfg(feature = "with-containers")]
- use rand::{distributions::Alphanumeric, thread_rng, Rng};
+ use rand::distributions::Alphanumeric;
+ use rand::{thread_rng, Rng};
use serial_test::serial;
use super::*;
- #[cfg(feature = "with-containers")]
+ use crate::test_container::SyncPureFtpRunner;
use crate::types::FormatControl;
use crate::AsyncFtpStream;
- #[cfg(feature = "async-native-tls")]
- use crate::{AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
- #[cfg(feature = "with-containers")]
#[async_attributes::test]
- #[serial]
async fn connect() {
crate::log_init();
- let stream = setup_stream().await;
+ let (stream, _container) = setup_stream().await;
finalize_stream(stream).await;
}
- /*
- #[async_attributes::test]
- #[cfg(feature = "async-native-tls")]
- #[serial]
- async fn should_connect_ssl_native_tls() {
- use crate::AsyncNativeTlsFtpStream;
-
- crate::log_init();
- let ftp_stream = AsyncNativeTlsFtpStream::connect("test.rebex.net:21")
- .await
- .unwrap();
- let mut ftp_stream = ftp_stream
- .into_secure(
- AsyncNativeTlsConnector::from(NativeTlsConnector::new()),
- "test.rebex.net",
- )
- .await
- .unwrap();
- // Set timeout (to test ref to ssl)
- assert!(ftp_stream.get_ref().await.set_ttl(255).is_ok());
- // Login
- assert!(ftp_stream.login("demo", "password").await.is_ok());
- // PWD
- assert_eq!(ftp_stream.pwd().await.unwrap().as_str(), "/");
- // Quit
- assert!(ftp_stream.quit().await.is_ok());
- }
-
- #[async_attributes::test]
- #[serial]
- #[cfg(all(feature = "async-native-tls", feature = "deprecated"))]
- async fn should_connect_ssl_implicit_native_tls() {
- crate::log_init();
- let mut ftp_stream = AsyncNativeTlsFtpStream::connect_secure_implicit(
- "test.rebex.net:990",
- AsyncNativeTlsConnector::from(NativeTlsConnector::new()),
- "test.rebex.net",
- )
- .await
- .unwrap();
- // Set timeout (to test ref to ssl)
- assert!(ftp_stream.get_ref().await.set_ttl(255).is_ok());
- // Login
- assert!(ftp_stream.login("demo", "password").await.is_ok());
- // PWD
- assert_eq!(ftp_stream.pwd().await.unwrap().as_str(), "/");
- // Quit
- assert!(ftp_stream.quit().await.is_ok());
- }
-
-
- #[async_attributes::test]
- #[cfg(feature = "async-native-tls")]
- #[serial]
- async fn should_work_after_clear_command_channel_native_tls() {
- crate::log_init();
- let mut ftp_stream = AsyncNativeTlsFtpStream::connect("test.rebex.net:21")
- .await
- .unwrap()
- .into_secure(
- AsyncNativeTlsConnector::from(NativeTlsConnector::new()),
- "test.rebex.net",
- )
- .await
- .unwrap()
- .clear_command_channel()
- .await
- .unwrap();
- // Login
- assert!(ftp_stream.login("demo", "password").await.is_ok());
- // CCC
- assert!(ftp_stream.pwd().await.is_ok());
- assert!(ftp_stream.list(None).await.is_ok());
- assert!(ftp_stream.quit().await.is_ok());
- }
-
- #[async_attributes::test]
- #[cfg(feature = "async-rustls")]
- #[serial]
- async fn should_connect_ssl_rustls() {
- crate::log_init();
- let ftp_stream = AsyncRustlsFtpStream::connect("ftp.uni-bayreuth.de:21")
- .await
- .unwrap();
- let mut ftp_stream = ftp_stream
- .into_secure(
- AsyncRustlsConnector::from(RustlsTlsConnector::new()),
- "ftp.uni-bayreuth.de",
- )
- .await
- .unwrap();
- // Set timeout (to test ref to ssl)
- assert!(ftp_stream.get_ref().await.set_ttl(255).is_ok());
- // Quit
- assert!(ftp_stream.quit().await.is_ok());
- }
- */
-
#[async_attributes::test]
#[serial]
async fn should_change_mode() {
@@ -1144,11 +1065,13 @@ mod test {
}
#[async_attributes::test]
- #[cfg(feature = "with-containers")]
- #[serial]
async fn should_connect_with_timeout() {
crate::log_init();
- let addr: SocketAddr = "127.0.0.1:10021".parse().expect("invalid hostname");
+ let container = SyncPureFtpRunner::start();
+ let port = container.get_ftp_port();
+ let url = format!("127.0.0.1:{port}");
+ let addr: SocketAddr = url.parse().expect("invalid hostname");
+
let mut stream = AsyncFtpStream::connect_timeout(addr, Duration::from_secs(15))
.await
.unwrap();
@@ -1160,11 +1083,9 @@ mod test {
}
#[async_attributes::test]
- #[cfg(feature = "with-containers")]
- #[serial]
async fn welcome_message() {
crate::log_init();
- let stream = setup_stream().await;
+ let (stream, _container) = setup_stream().await;
assert!(stream
.get_welcome_msg()
.unwrap()
@@ -1173,32 +1094,27 @@ mod test {
}
#[async_attributes::test]
- #[cfg(feature = "with-containers")]
- #[serial]
+
async fn should_set_passive_nat_workaround() {
crate::log_init();
- let mut stream = setup_stream().await;
+ let (mut stream, _container) = setup_stream().await;
stream.set_passive_nat_workaround(true);
assert!(stream.nat_workaround);
finalize_stream(stream).await;
}
#[async_attributes::test]
- #[cfg(feature = "with-containers")]
- #[serial]
+
async fn get_ref() {
- crate::log_init();
- let stream = setup_stream().await;
+ let (stream, _container) = setup_stream().await;
assert!(stream.get_ref().await.set_ttl(255).is_ok());
finalize_stream(stream).await;
}
#[async_attributes::test]
- #[cfg(feature = "with-containers")]
- #[serial]
+
async fn change_wrkdir() {
- crate::log_init();
- let mut stream = setup_stream().await;
+ let (mut stream, _container) = setup_stream().await;
let wrkdir: String = stream.pwd().await.unwrap();
assert!(stream.cwd("/").await.is_ok());
assert_eq!(stream.pwd().await.unwrap().as_str(), "/");
@@ -1207,11 +1123,9 @@ mod test {
}
#[async_attributes::test]
- #[cfg(feature = "with-containers")]
- #[serial]
+
async fn cd_up() {
- crate::log_init();
- let mut stream = setup_stream().await;
+ let (mut stream, _container) = setup_stream().await;
let wrkdir: String = stream.pwd().await.unwrap();
assert!(stream.cdup().await.is_ok());
assert_eq!(stream.pwd().await.unwrap().as_str(), "/");
@@ -1220,21 +1134,17 @@ mod test {
}
#[async_attributes::test]
- #[cfg(feature = "with-containers")]
- #[serial]
+
async fn noop() {
- crate::log_init();
- let mut stream = setup_stream().await;
+ let (mut stream, _container) = setup_stream().await;
assert!(stream.noop().await.is_ok());
finalize_stream(stream).await;
}
#[async_attributes::test]
- #[cfg(feature = "with-containers")]
- #[serial]
+
async fn make_and_remove_dir() {
- crate::log_init();
- let mut stream = setup_stream().await;
+ let (mut stream, _container) = setup_stream().await;
// Make directory
assert!(stream.mkdir("omar").await.is_ok());
// It shouldn't allow me to re-create the directory; should return error code 550
@@ -1250,11 +1160,9 @@ mod test {
}
#[async_attributes::test]
- #[cfg(feature = "with-containers")]
- #[serial]
+
async fn should_get_feat_and_set_opts() {
- crate::log_init();
- let mut stream = setup_stream().await;
+ let (mut stream, _container) = setup_stream().await;
assert!(stream.feat().await.is_ok());
assert!(stream.opts("UTF8", Some("ON")).await.is_ok());
@@ -1262,11 +1170,9 @@ mod test {
}
#[async_attributes::test]
- #[cfg(feature = "with-containers")]
- #[serial]
+
async fn set_transfer_type() {
- crate::log_init();
- let mut stream = setup_stream().await;
+ let (mut stream, _container) = setup_stream().await;
assert!(stream.transfer_type(FileType::Binary).await.is_ok());
assert!(stream
.transfer_type(FileType::Ascii(FormatControl::Default))
@@ -1276,13 +1182,11 @@ mod test {
}
#[async_attributes::test]
- #[cfg(feature = "with-containers")]
- #[serial]
+
async fn transfer_file() {
- crate::log_init();
use async_std::io::Cursor;
- let mut stream = setup_stream().await;
+ let (mut stream, _container) = setup_stream().await;
// Set transfer type to Binary
assert!(stream.transfer_type(FileType::Binary).await.is_ok());
// Write file
@@ -1329,11 +1233,8 @@ mod test {
}
#[async_attributes::test]
- #[serial]
- #[cfg(feature = "with-containers")]
async fn should_resume_transfer() {
- crate::log_init();
- let mut stream = setup_stream().await;
+ let (mut stream, container) = setup_stream().await;
// Set transfer type to Binary
assert!(stream.transfer_type(FileType::Binary).await.is_ok());
// get dir
@@ -1351,10 +1252,36 @@ mod test {
drop(stream);
drop(transfer_stream);
// Re-connect to server
- let mut stream = ImplAsyncFtpStream::connect("127.0.0.1:10021")
- .await
- .unwrap();
+ let port = container.get_ftp_port();
+ let url = format!("localhost:{port}");
+
+ let mut stream = AsyncFtpStream::connect(url).await.unwrap();
assert!(stream.login("test", "test").await.is_ok());
+ // Create wrkdir
+ let tempdir: String = generate_tempdir();
+ assert!(stream.mkdir(tempdir.as_str()).await.is_ok());
+ // Change directory
+ assert!(stream.cwd(tempdir.as_str()).await.is_ok());
+
+ let container_t = container.clone();
+
+ let mut stream = stream.passive_stream_builder(move |addr| {
+ let container_t = container_t.clone();
+ Box::pin(async move {
+ let mut addr = addr.clone();
+ let port = addr.port();
+ let mapped = container_t.get_mapped_port(port);
+
+ addr.set_port(mapped);
+
+ info!("mapped port {port} to {mapped} for PASV");
+
+ // open stream to this address instead
+ TcpStream::connect(addr)
+ .await
+ .map_err(FtpError::ConnectionError)
+ })
+ });
// Go back to previous dir
assert!(stream.cwd(wrkdir).await.is_ok());
// Set transfer type to Binary
@@ -1373,7 +1300,7 @@ mod test {
// Finalize
assert!(stream.finalize_put_stream(transfer_stream).await.is_ok());
// Get size
- assert_eq!(stream.size("test.bin").await.unwrap(), 11);
+ //assert_eq!(stream.size("test.bin").await.unwrap(), 11);
// Remove file
assert!(stream.rm("test.bin").await.is_ok());
// Drop stream
@@ -1381,13 +1308,12 @@ mod test {
}
#[async_attributes::test]
- #[serial]
- #[cfg(feature = "with-containers")]
+
async fn should_transfer_file_with_extended_passive_mode() {
crate::log_init();
use async_std::io::Cursor;
- let mut stream = setup_stream().await;
+ let (mut stream, _container) = setup_stream().await;
// Set transfer type to Binary
assert!(stream.transfer_type(FileType::Binary).await.is_ok());
stream.set_mode(Mode::ExtendedPassive);
@@ -1400,26 +1326,104 @@ mod test {
finalize_stream(stream).await;
}
- // -- test utils
+ #[async_attributes::test]
+ async fn test_should_set_passive_stream_builder() {
+ 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 if the stream is Send
+ fn is_send(_send: T) {}
+
+ #[async_attributes::test]
+ async fn test_ftp_stream_should_be_send() {
+ 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)
+ })
+ });
- #[cfg(feature = "with-containers")]
- async fn setup_stream() -> crate::AsyncFtpStream {
+ is_send::(ftp_stream);
+ }
+
+ /// Test if the stream is Sync
+ fn is_sync(_send: T) {}
+
+ #[async_attributes::test]
+ async fn test_ftp_stream_should_be_sync() {
crate::log_init();
- let mut ftp_stream = ImplAsyncFtpStream::connect("127.0.0.1:10021")
+ let ftp_stream = AsyncFtpStream::connect("test.rebex.net:21")
.await
- .unwrap();
+ .unwrap()
+ .passive_stream_builder(|addr| {
+ Box::pin(async move {
+ println!("Connecting to {}", addr);
+ TcpStream::connect(addr)
+ .await
+ .map_err(FtpError::ConnectionError)
+ })
+ });
+
+ is_sync::(ftp_stream);
+ }
+
+ // -- test utils
+
+ async fn setup_stream() -> (crate::AsyncFtpStream, Arc) {
+ crate::log_init();
+ let container = Arc::new(SyncPureFtpRunner::start());
+
+ let port = container.get_ftp_port();
+ let url = format!("localhost:{port}");
+
+ let mut ftp_stream = ImplAsyncFtpStream::connect(url).await.unwrap();
assert!(ftp_stream.login("test", "test").await.is_ok());
// Create wrkdir
let tempdir: String = generate_tempdir();
assert!(ftp_stream.mkdir(tempdir.as_str()).await.is_ok());
// Change directory
assert!(ftp_stream.cwd(tempdir.as_str()).await.is_ok());
- ftp_stream
+
+ let container_t = container.clone();
+
+ let ftp_stream = ftp_stream.passive_stream_builder(move |addr| {
+ let container_t = container_t.clone();
+ Box::pin(async move {
+ let mut addr = addr.clone();
+ let port = addr.port();
+ let mapped = container_t.get_mapped_port(port);
+
+ addr.set_port(mapped);
+
+ info!("mapped port {port} to {mapped} for PASV");
+
+ // open stream to this address instead
+ TcpStream::connect(addr)
+ .await
+ .map_err(FtpError::ConnectionError)
+ })
+ });
+
+ (ftp_stream, container)
}
- #[cfg(feature = "with-containers")]
async fn finalize_stream(mut stream: crate::AsyncFtpStream) {
- crate::log_init();
// Get working directory
let wrkdir: String = stream.pwd().await.unwrap();
// Remove directory
@@ -1427,7 +1431,6 @@ mod test {
assert!(stream.quit().await.is_ok());
}
- #[cfg(feature = "with-containers")]
fn generate_tempdir() -> String {
let mut rng = thread_rng();
let name: String = std::iter::repeat(())
diff --git a/suppaftp/src/async_ftp/tls/rustls.rs b/suppaftp/src/async_ftp/tls/rustls.rs
index 78e9f7a..5c82e7f 100644
--- a/suppaftp/src/async_ftp/tls/rustls.rs
+++ b/suppaftp/src/async_ftp/tls/rustls.rs
@@ -6,10 +6,11 @@ use std::pin::Pin;
use async_std::io::{Read, Write};
use async_std::net::TcpStream;
-use async_tls::client::TlsStream;
-use async_tls::TlsConnector as RustlsTlsConnector;
use async_trait::async_trait;
+use futures_rustls::client::TlsStream;
+use futures_rustls::TlsConnector as RustlsTlsConnector;
use pin_project::pin_project;
+use rustls_pki_types::{DnsName, ServerName};
use super::{AsyncTlsConnector, AsyncTlsStream};
use crate::{FtpError, FtpResult};
@@ -36,8 +37,13 @@ impl AsyncTlsConnector for AsyncRustlsConnector {
type Stream = AsyncRustlsStream;
async fn connect(&self, domain: &str, stream: TcpStream) -> FtpResult {
+ let server_name = ServerName::DnsName(
+ DnsName::try_from(domain.to_string())
+ .map_err(|e| FtpError::SecureError(e.to_string()))?,
+ );
+
self.connector
- .connect(domain, stream)
+ .connect(server_name, stream)
.await
.map(AsyncRustlsStream::from)
.map_err(|e| FtpError::SecureError(e.to_string()))
@@ -95,7 +101,7 @@ impl AsyncTlsStream for AsyncRustlsStream {
type InnerStream = TlsStream;
fn get_ref(&self) -> &TcpStream {
- self.stream.get_ref()
+ self.stream.get_ref().0
}
fn mut_ref(&mut self) -> &mut Self::InnerStream {
@@ -103,6 +109,6 @@ impl AsyncTlsStream for AsyncRustlsStream {
}
fn tcp_stream(self) -> TcpStream {
- self.stream.get_ref().clone()
+ self.stream.get_ref().0.clone()
}
}
diff --git a/suppaftp/src/lib.rs b/suppaftp/src/lib.rs
index 7b028a9..23ff698 100644
--- a/suppaftp/src/lib.rs
+++ b/suppaftp/src/lib.rs
@@ -143,6 +143,9 @@ mod sync_ftp;
pub mod list;
pub mod types;
+#[cfg(test)]
+mod test_container;
+
// -- secure deps
#[cfg(feature = "native-tls")]
// #[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
@@ -221,5 +224,14 @@ pub type AsyncRustlsFtpStream = ImplAsyncFtpStream;
// -- test logging
#[cfg(test)]
pub fn log_init() {
- let _ = env_logger::builder().is_test(true).try_init();
+ use std::sync::Once;
+
+ static INIT: Once = Once::new();
+
+ INIT.call_once(|| {
+ let _ = env_logger::builder()
+ .filter_level(log::LevelFilter::Trace)
+ .is_test(true)
+ .try_init();
+ });
}
diff --git a/suppaftp/src/list.rs b/suppaftp/src/list.rs
index 3afe8cd..ece6596 100644
--- a/suppaftp/src/list.rs
+++ b/suppaftp/src/list.rs
@@ -146,7 +146,7 @@ impl File {
self.size
}
- //// Returns the last time the file was modified
+ /// Returns the last time the file was modified
pub fn modified(&self) -> SystemTime {
self.modified
}
diff --git a/suppaftp/src/sync_ftp/mod.rs b/suppaftp/src/sync_ftp/mod.rs
index 8dca29b..f8de664 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 + Sync;
+
/// 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 + Sync + '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()))?;
@@ -750,6 +770,7 @@ where
match data_stream.read_line(&mut line) {
Ok(0) => break,
Ok(_) => {
+ trace!("STREAM IN: {:?}", line);
if line.ends_with('\n') {
line.pop();
if line.ends_with('\r') {
@@ -761,7 +782,10 @@ where
}
lines.push(line);
}
- Err(_) => return Err(FtpError::BadResponse),
+ Err(err) => {
+ error!("failed to get lines from stream: {err}");
+ return Err(FtpError::BadResponse);
+ }
}
}
trace!("Lines from stream {:?}", lines);
@@ -872,11 +896,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"))]
@@ -949,8 +973,28 @@ where
debug!("PASV command");
self.perform(Command::Pasv)?;
// PASV response format : 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
- let response: Response = self.read_response(Status::PassiveMode)?;
+ let response = self.read_response(Status::PassiveMode)?;
+ let addr = Self::parse_passive_address_from_response(response)?;
+ trace!("Passive address: {addr}",);
+ if self.nat_workaround {
+ let mut remote = self
+ .reader
+ .get_ref()
+ .get_ref()
+ .peer_addr()
+ .map_err(FtpError::ConnectionError)?;
+ remote.set_port(addr.port());
+ trace!("Replacing site local address {} with {}", addr, remote);
+ Ok(remote)
+ } else {
+ Ok(addr)
+ }
+ }
+
+ /// Parse passive address from response
+ pub(crate) fn parse_passive_address_from_response(response: Response) -> FtpResult {
let response_str = response.as_string().map_err(|_| FtpError::BadResponse)?;
+ trace!("PASV response: {response_str}",);
let caps = PASV_PORT_RE
.captures(&response_str)
.ok_or_else(|| FtpError::UnexpectedResponse(response.clone()))?;
@@ -968,20 +1012,8 @@ where
let ip = Ipv4Addr::new(oct1, oct2, oct3, oct4);
let port = (u16::from(msb) << 8) | u16::from(lsb);
let addr = SocketAddr::new(ip.into(), port);
- trace!("Passive address: {}", addr);
- if self.nat_workaround && ip.is_private() {
- let mut remote = self
- .reader
- .get_ref()
- .get_ref()
- .peer_addr()
- .map_err(FtpError::ConnectionError)?;
- remote.set_port(port);
- trace!("Replacing site local address {} with {}", addr, remote);
- Ok(remote)
- } else {
- Ok(addr)
- }
+
+ Ok(addr)
}
/// Execute a command which returns list of strings in a separate stream
@@ -992,127 +1024,70 @@ 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)]
mod test {
- #[cfg(any(feature = "with-containers", feature = "secure"))]
+ use std::net::IpAddr;
+ use std::sync::Arc;
+
+ #[cfg(feature = "secure")]
use pretty_assertions::assert_eq;
- #[cfg(feature = "with-containers")]
- use rand::{distributions::Alphanumeric, thread_rng, Rng};
+ use rand::distributions::Alphanumeric;
+ use rand::{thread_rng, Rng};
use serial_test::serial;
use super::*;
- #[cfg(feature = "with-containers")]
+ use crate::test_container::SyncPureFtpRunner;
use crate::types::FormatControl;
use crate::FtpStream;
#[test]
- #[cfg(feature = "with-containers")]
fn connect() {
crate::log_init();
- let stream: FtpStream = setup_stream();
- finalize_stream(stream);
- }
-
- /*
- #[test]
- #[serial]
- #[cfg(feature = "native-tls")]
- fn should_connect_ssl_native_tls() {
- crate::log_init();
- use std::time::Duration;
- let ftp_stream = crate::NativeTlsFtpStream::connect("test.rebex.net:21").unwrap();
- let mut ftp_stream = ftp_stream
- .into_secure(
- NativeTlsConnector::from(TlsConnector::new().unwrap()),
- "test.rebex.net",
- )
- .unwrap();
- // Set timeout (to test ref to ssl)
- assert!(ftp_stream
- .get_ref()
- .set_read_timeout(Some(Duration::from_secs(10)))
- .is_ok());
- // Login
- assert!(ftp_stream.login("demo", "password").is_ok());
- // PWD
- assert_eq!(ftp_stream.pwd().unwrap().as_str(), "/");
- // Quit
- assert!(ftp_stream.quit().is_ok());
- }
- */
-
- /*
- #[test]
- #[serial]
- #[cfg(feature = "native-tls")]
- fn should_work_after_clear_command_channel_native_tls() {
- crate::log_init();
- let mut ftp_stream = crate::NativeTlsFtpStream::connect("test.rebex.net:21")
- .unwrap()
- .into_secure(
- NativeTlsConnector::from(TlsConnector::new().unwrap()),
- "test.rebex.net",
- )
- .unwrap()
- .clear_command_channel()
- .unwrap();
- // Login
- assert!(ftp_stream.login("demo", "password").is_ok());
- // CCC
- assert!(ftp_stream.pwd().is_ok());
- assert!(ftp_stream.list(None).is_ok());
- assert!(ftp_stream.quit().is_ok());
+ with_test_ftp_stream(|_stream| {});
}
-
#[test]
- #[serial]
- #[cfg(all(feature = "native-tls", feature = "deprecated"))]
- fn should_connect_ssl_implicit_native_tls() {
- use std::time::Duration;
- crate::log_init();
- let mut ftp_stream = crate::NativeTlsFtpStream::connect_secure_implicit(
- "test.rebex.net:990",
- NativeTlsConnector::from(TlsConnector::new().unwrap()),
- "test.rebex.net",
- )
- .unwrap();
- // Set timeout (to test ref to ssl)
- assert!(ftp_stream
- .get_ref()
- .set_read_timeout(Some(Duration::from_secs(10)))
- .is_ok());
- // Login
- assert!(ftp_stream.login("demo", "password").is_ok());
- // PWD
- assert_eq!(ftp_stream.pwd().unwrap().as_str(), "/");
- // Quit
- assert!(ftp_stream.quit().is_ok());
- }
-
+ fn test_should_parse_passive_address_from_response() {
+ let response = vec![
+ 50, 50, 55, 32, 69, 110, 116, 101, 114, 105, 110, 103, 32, 80, 97, 115, 115, 105, 118,
+ 101, 32, 77, 111, 100, 101, 32, 40, 49, 50, 55, 44, 48, 44, 48, 44, 49, 44, 49, 49, 55,
+ 44, 53, 54, 41, 13, 10,
+ ];
+ let response = Response::new(Status::PassiveMode, response);
+
+ let address = FtpStream::parse_passive_address_from_response(response)
+ .expect("Failed to parse passive address");
+ assert_eq!(
+ address.ip(),
+ IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
+ "IP address is not correct"
+ );
+ assert_eq!(address.port(), 30008, "Port is not correct");
- #[test]
- #[serial]
- #[cfg(feature = "rustls")]
- fn should_connect_ssl_rustls() {
- use super::tls::RustlsConnector;
+ let response = vec![
+ 50, 50, 55, 32, 69, 110, 116, 101, 114, 105, 110, 103, 32, 80, 97, 115, 115, 105, 118,
+ 101, 32, 77, 111, 100, 101, 32, 40, 53, 56, 44, 50, 52, 55, 44, 57, 50, 44, 49, 50, 50,
+ 44, 49, 52, 54, 44, 50, 51, 57, 41, 46, 13, 10,
+ ];
+ let response = Response::new(Status::PassiveMode, response);
- crate::log_init();
- let config = Arc::new(rustls_config());
- let mut ftp_stream = crate::RustlsFtpStream::connect("ftp.uni-bayreuth.de:21")
- .unwrap()
- .into_secure(
- RustlsConnector::from(Arc::clone(&config)),
- "ftp.uni-bayreuth.de",
- )
- .unwrap();
- // Quit
- assert!(ftp_stream.quit().is_ok());
+ let address = FtpStream::parse_passive_address_from_response(response)
+ .expect("Failed to parse passive address");
+ assert_eq!(
+ address.ip(),
+ IpAddr::V4(Ipv4Addr::new(58, 247, 92, 122)),
+ "IP address is not correct"
+ );
+ assert_eq!(address.port(), 37615, "Port is not correct");
}
- */
#[test]
#[serial]
@@ -1128,11 +1103,14 @@ mod test {
}
#[test]
- #[serial]
- #[cfg(feature = "with-containers")]
+
fn should_connect_with_timeout() {
crate::log_init();
- let addr: SocketAddr = "127.0.0.1:10021".parse().expect("invalid hostname");
+ let container = SyncPureFtpRunner::start();
+ let port = container.get_ftp_port();
+ let url = format!("127.0.0.1:{port}");
+ let addr: SocketAddr = url.parse().expect("invalid hostname");
+
let mut stream = FtpStream::connect_timeout(addr, Duration::from_secs(15)).unwrap();
assert!(stream.login("test", "test").is_ok());
assert!(stream
@@ -1142,214 +1120,168 @@ mod test {
}
#[test]
- #[serial]
- #[cfg(feature = "with-containers")]
+
fn welcome_message() {
crate::log_init();
- let stream: FtpStream = setup_stream();
- assert!(stream
- .get_welcome_msg()
- .unwrap()
- .contains("220 You will be disconnected after 15 minutes of inactivity."));
- finalize_stream(stream);
+ with_test_ftp_stream(|stream| {
+ assert!(stream
+ .get_welcome_msg()
+ .unwrap()
+ .contains("220 You will be disconnected after 15 minutes of inactivity."));
+ });
}
#[test]
- #[serial]
- #[cfg(feature = "with-containers")]
+
fn should_set_passive_nat_workaround() {
- crate::log_init();
- let mut stream: FtpStream = setup_stream();
- stream.set_passive_nat_workaround(true);
- assert!(stream.nat_workaround);
- finalize_stream(stream);
+ with_test_ftp_stream(|stream| {
+ stream.set_passive_nat_workaround(true);
+ assert!(stream.nat_workaround);
+ });
}
#[test]
- #[serial]
- #[cfg(feature = "with-containers")]
fn get_ref() {
use std::time::Duration;
- crate::log_init();
- let stream: FtpStream = setup_stream();
- assert!(stream
- .get_ref()
- .set_read_timeout(Some(Duration::from_secs(10)))
- .is_ok());
- finalize_stream(stream);
+ with_test_ftp_stream(|stream| {
+ assert!(stream
+ .get_ref()
+ .set_read_timeout(Some(Duration::from_secs(10)))
+ .is_ok());
+ });
}
#[test]
- #[serial]
- #[cfg(feature = "with-containers")]
+
fn change_wrkdir() {
- crate::log_init();
- let mut stream: FtpStream = setup_stream();
- let wrkdir: String = stream.pwd().unwrap();
- assert!(stream.cwd("/").is_ok());
- assert_eq!(stream.pwd().unwrap().as_str(), "/");
- assert!(stream.cwd(wrkdir.as_str()).is_ok());
- finalize_stream(stream);
+ with_test_ftp_stream(|stream| {
+ let wrkdir: String = stream.pwd().unwrap();
+ assert!(stream.cwd("/").is_ok());
+ assert_eq!(stream.pwd().unwrap().as_str(), "/");
+ assert!(stream.cwd(wrkdir.as_str()).is_ok());
+ })
}
#[test]
- #[serial]
- #[cfg(feature = "with-containers")]
+
fn cd_up() {
- crate::log_init();
- let mut stream: FtpStream = setup_stream();
- let wrkdir: String = stream.pwd().unwrap();
- assert!(stream.cdup().is_ok());
- assert_eq!(stream.pwd().unwrap().as_str(), "/");
- assert!(stream.cwd(wrkdir.as_str()).is_ok());
- finalize_stream(stream);
+ with_test_ftp_stream(|stream| {
+ let wrkdir: String = stream.pwd().unwrap();
+ assert!(stream.cdup().is_ok());
+ assert_eq!(stream.pwd().unwrap().as_str(), "/");
+ assert!(stream.cwd(wrkdir.as_str()).is_ok());
+ })
}
#[test]
- #[serial]
- #[cfg(feature = "with-containers")]
+
fn noop() {
- crate::log_init();
- let mut stream: FtpStream = setup_stream();
- assert!(stream.noop().is_ok());
- finalize_stream(stream);
+ with_test_ftp_stream(|stream| {
+ assert!(stream.noop().is_ok());
+ })
}
#[test]
- #[serial]
- #[cfg(feature = "with-containers")]
+
fn make_and_remove_dir() {
- crate::log_init();
- let mut stream: FtpStream = setup_stream();
- // Make directory
- assert!(stream.mkdir("omar").is_ok());
- // It shouldn't allow me to re-create the directory; should return error code 550
- match stream.mkdir("omar").err().unwrap() {
- FtpError::UnexpectedResponse(Response { status, body: _ }) => {
- assert_eq!(status, Status::FileUnavailable)
+ with_test_ftp_stream(|stream| {
+ // Make directory
+ assert!(stream.mkdir("omar").is_ok());
+ // It shouldn't allow me to re-create the directory; should return error code 550
+ match stream.mkdir("omar").err().unwrap() {
+ FtpError::UnexpectedResponse(Response { status, body: _ }) => {
+ assert_eq!(status, Status::FileUnavailable)
+ }
+ err => panic!("Expected UnexpectedResponse, got {}", err),
}
- err => panic!("Expected UnexpectedResponse, got {}", err),
- }
- // Remove directory
- assert!(stream.rmdir("omar").is_ok());
- finalize_stream(stream);
+ // Remove directory
+ assert!(stream.rmdir("omar").is_ok());
+ })
}
#[test]
- #[serial]
- #[cfg(feature = "with-containers")]
+
fn set_transfer_type() {
- crate::log_init();
- let mut stream: FtpStream = setup_stream();
- assert!(stream.transfer_type(FileType::Binary).is_ok());
- assert!(stream
- .transfer_type(FileType::Ascii(FormatControl::Default))
- .is_ok());
- finalize_stream(stream);
+ with_test_ftp_stream(|stream| {
+ assert!(stream.transfer_type(FileType::Binary).is_ok());
+ assert!(stream
+ .transfer_type(FileType::Ascii(FormatControl::Default))
+ .is_ok());
+ })
}
#[test]
- #[serial]
- #[cfg(feature = "with-containers")]
fn should_transfer_file() {
- crate::log_init();
- let mut stream: FtpStream = setup_stream();
- // Set transfer type to Binary
- assert!(stream.transfer_type(FileType::Binary).is_ok());
- // Write file
- let file_data = "test data\n";
- let mut reader = Cursor::new(file_data.as_bytes());
- assert!(stream.put_file("test.txt", &mut reader).is_ok());
- // Read file
- assert_eq!(
- stream
- .retr_as_buffer("test.txt")
- .map(|bytes| bytes.into_inner())
- .unwrap(),
- file_data.as_bytes()
- );
- // Get size
- assert_eq!(stream.size("test.txt").unwrap(), 10);
- // Size of non-existing file
- assert!(stream.size("omarone.txt").is_err());
- // List directory
- assert_eq!(stream.list(None).unwrap().len(), 1);
- // list names
- assert_eq!(stream.nlst(None).unwrap().as_slice(), &["test.txt"]);
- // modification time
- assert!(stream.mdtm("test.txt").is_ok());
- // Remove file
- assert!(stream.rm("test.txt").is_ok());
- assert!(stream.mdtm("test.txt").is_err());
- // Write file, rename and get
- let file_data = "test data\n";
- let mut reader = Cursor::new(file_data.as_bytes());
- assert!(stream.put_file("test.txt", &mut reader).is_ok());
- // Append file
- let mut reader = Cursor::new(file_data.as_bytes());
- assert!(stream.append_file("test.txt", &mut reader).is_ok());
- // Read file
- let mut reader = stream.retr_as_stream("test.txt").unwrap();
- let mut buffer = Vec::new();
- assert!(reader.read_to_end(&mut buffer).is_ok());
- // Finalize
- assert!(stream.finalize_retr_stream(Box::new(reader)).is_ok());
- // Verify file matches
- assert_eq!(buffer.as_slice(), "test data\ntest data\n".as_bytes());
- // Rename
- assert!(stream.rename("test.txt", "toast.txt").is_ok());
- assert!(stream.rm("toast.txt").is_ok());
- // List directory again
- assert_eq!(stream.list(None).unwrap().len(), 0);
- finalize_stream(stream);
+ with_test_ftp_stream(|stream| {
+ // Set transfer type to Binary
+ assert!(stream.transfer_type(FileType::Binary).is_ok());
+ // Write file
+ let file_data = "test data\n";
+ let mut reader = Cursor::new(file_data.as_bytes());
+ assert!(stream.put_file("test.txt", &mut reader).is_ok());
+ // Read file
+ assert_eq!(
+ stream
+ .retr_as_buffer("test.txt")
+ .map(|bytes| bytes.into_inner())
+ .unwrap(),
+ file_data.as_bytes()
+ );
+ // Get size
+ assert_eq!(stream.size("test.txt").unwrap(), 10);
+ // Size of non-existing file
+ assert!(stream.size("omarone.txt").is_err());
+ // List directory
+ assert_eq!(stream.list(None).unwrap().len(), 1);
+ // list names
+ assert_eq!(stream.nlst(None).unwrap().as_slice(), &["test.txt"]);
+ // modification time
+ assert!(stream.mdtm("test.txt").is_ok());
+ // Remove file
+ assert!(stream.rm("test.txt").is_ok());
+ assert!(stream.mdtm("test.txt").is_err());
+ // Write file, rename and get
+ let file_data = "test data\n";
+ let mut reader = Cursor::new(file_data.as_bytes());
+ assert!(stream.put_file("test.txt", &mut reader).is_ok());
+ // Append file
+ let mut reader = Cursor::new(file_data.as_bytes());
+ assert!(stream.append_file("test.txt", &mut reader).is_ok());
+ // Read file
+ let mut reader = stream.retr_as_stream("test.txt").unwrap();
+ let mut buffer = Vec::new();
+ assert!(reader.read_to_end(&mut buffer).is_ok());
+ // Finalize
+ assert!(stream.finalize_retr_stream(Box::new(reader)).is_ok());
+ // Verify file matches
+ assert_eq!(buffer.as_slice(), "test data\ntest data\n".as_bytes());
+ // Rename
+ assert!(stream.rename("test.txt", "toast.txt").is_ok());
+ assert!(stream.rm("toast.txt").is_ok());
+ // List directory again
+ assert_eq!(stream.list(None).unwrap().len(), 0);
+ })
}
- /*
#[test]
- #[cfg(feature = "with-containers")]
- #[serial]
- fn should_abort_transfer() {
- crate::log_init();
- let mut stream: FtpStream = setup_stream();
- // Set transfer type to Binary
- assert!(stream.transfer_type(FileType::Binary).is_ok());
- // put as stream
- let mut transfer_stream = stream.put_with_stream("test.bin").unwrap();
- assert_eq!(
- transfer_stream
- .write(&[0x00, 0x01, 0x02, 0x03, 0x04])
- .unwrap(),
- 5
- );
- // Abort
- assert!(stream.abort(transfer_stream).is_ok());
- // Check whether other commands still work after transfer
- assert!(stream.rm("test.bin").is_ok());
- // Check whether data channel still works
- assert!(stream.list(None).is_ok());
- finalize_stream(stream);
+ fn should_get_feat_and_set_opts() {
+ with_test_ftp_stream(|stream| {
+ assert!(stream.feat().is_ok());
+ assert!(stream.opts("UTF8", Some("ON")).is_ok());
+ })
}
- */
#[test]
- #[cfg(feature = "with-containers")]
- #[serial]
- fn should_get_feat_and_set_opts() {
+ fn should_resume_transfer() {
crate::log_init();
- let mut stream: FtpStream = setup_stream();
+ let container = Arc::new(SyncPureFtpRunner::start());
+ let port = container.get_ftp_port();
- assert!(stream.feat().is_ok());
- assert!(stream.opts("UTF8", Some("ON")).is_ok());
+ let url = format!("localhost:{port}");
- finalize_stream(stream);
- }
+ // init stream with mapper
+ let mut stream: FtpStream = setup_stream(&url, &container);
- #[test]
- #[serial]
- #[cfg(feature = "with-containers")]
- fn should_resume_transfer() {
- crate::log_init();
- let mut stream: FtpStream = setup_stream();
// Set transfer type to Binary
assert!(stream.transfer_type(FileType::Binary).is_ok());
// get dir
@@ -1366,8 +1298,7 @@ mod test {
drop(stream);
drop(transfer_stream);
// Re-connect to server
- let mut stream = FtpStream::connect("127.0.0.1:10021").unwrap();
- assert!(stream.login("test", "test").is_ok());
+ let mut stream = setup_stream(&url, &container);
// Go back to previous dir
assert!(stream.cwd(wrkdir).is_ok());
// Set transfer type to Binary
@@ -1385,46 +1316,72 @@ mod test {
// Finalize
assert!(stream.finalize_put_stream(transfer_stream).is_ok());
// Get size
- assert_eq!(stream.size("test.bin").unwrap(), 11);
+ //assert_eq!(stream.size("test.bin").unwrap(), 11);
// Remove file
assert!(stream.rm("test.bin").is_ok());
- // Drop stream
+
finalize_stream(stream);
}
#[test]
- #[serial]
- #[cfg(feature = "with-containers")]
- fn should_transfer_file_with_extended_passive_mode() {
- crate::log_init();
- let mut stream: FtpStream = setup_stream();
- // Set transfer type to Binary
- assert!(stream.transfer_type(FileType::Binary).is_ok());
- stream.set_mode(Mode::ExtendedPassive);
- // Write file
- let file_data = "test data\n";
- let mut reader = Cursor::new(file_data.as_bytes());
- assert!(stream.put_file("test.txt", &mut reader).is_ok());
- // Remove file
- assert!(stream.rm("test.txt").is_ok());
- finalize_stream(stream);
+ fn should_transfer_with_extended_passive_mode() {
+ with_test_ftp_stream(|stream| {
+ // Set transfer type to Binary
+ assert!(stream.transfer_type(FileType::Binary).is_ok());
+ stream.set_mode(Mode::ExtendedPassive);
+ // Write file
+ let file_data = "test data\n";
+ let mut reader = Cursor::new(file_data.as_bytes());
+ assert!(stream.put_file("test.txt", &mut reader).is_ok());
+ // Remove file
+ assert!(stream.rm("test.txt").is_ok());
+ })
}
// -- test utils
- #[cfg(feature = "with-containers")]
- fn setup_stream() -> FtpStream {
- let mut ftp_stream = FtpStream::connect("127.0.0.1:10021").unwrap();
+ fn with_test_ftp_stream(f: F)
+ where
+ F: FnOnce(&mut FtpStream),
+ {
+ crate::log_init();
+ let container = Arc::new(SyncPureFtpRunner::start());
+ let port = container.get_ftp_port();
+
+ // init stream with mapper
+ let mut stream: FtpStream = setup_stream(&format!("localhost:{port}"), &container);
+
+ f(&mut stream);
+ finalize_stream(stream);
+
+ drop(container);
+ }
+
+ fn setup_stream(url: &str, container: &Arc) -> FtpStream {
+ let mut ftp_stream = FtpStream::connect(url).unwrap();
assert!(ftp_stream.login("test", "test").is_ok());
// Create wrkdir
let tempdir: String = generate_tempdir();
assert!(ftp_stream.mkdir(tempdir.as_str()).is_ok());
// Change directory
assert!(ftp_stream.cwd(tempdir.as_str()).is_ok());
- ftp_stream
+
+ let container_t = container.clone();
+
+ ftp_stream.passive_stream_builder(move |addr| {
+ let mut addr = addr.clone();
+ let port = addr.port();
+ let mapped = container_t.get_mapped_port(port);
+
+ addr.set_port(mapped);
+
+ info!("mapped port {port} to {mapped} for PASV");
+
+ // open stream to this address instead
+ TcpStream::connect(addr).map_err(FtpError::ConnectionError)
+ })
}
- #[cfg(feature = "with-containers")]
fn finalize_stream(mut stream: FtpStream) {
// Get working directory
let wrkdir: String = stream.pwd().unwrap();
@@ -1433,7 +1390,6 @@ mod test {
assert!(stream.quit().is_ok());
}
- #[cfg(feature = "with-containers")]
fn generate_tempdir() -> String {
let mut rng = thread_rng();
let name: String = std::iter::repeat(())
@@ -1444,21 +1400,45 @@ mod test {
format!("temp_{}", name)
}
- /*
- #[cfg(feature = "rustls")]
- fn rustls_config() -> ClientConfig {
- let mut root_store = rustls::RootCertStore::empty();
- root_store.add_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| {
- rustls::OwnedTrustAnchor::from_subject_spki_name_constraints(
- ta.subject,
- ta.spki,
- ta.name_constraints,
- )
- }));
- ClientConfig::builder()
- .with_safe_defaults()
- .with_root_certificates(root_store)
- .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)
+ });
+ }
+
+ /// Test if the stream is Send
+ fn is_send(_send: T) {}
+
+ fn is_sync(_sync: T) {}
+
+ #[test]
+ fn test_ftp_stream_should_be_send() {
+ 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)
+ });
+
+ is_send::(ftp_stream);
+ }
+
+ #[test]
+ fn test_ftp_stream_should_be_sync() {
+ 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)
+ });
+
+ is_sync::(ftp_stream);
+ }
}
diff --git a/suppaftp/src/sync_ftp/tls/rustls.rs b/suppaftp/src/sync_ftp/tls/rustls.rs
index f630371..a2cda8d 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,12 +33,15 @@ 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);
- Ok(RustlsStream { stream })
+ Ok(RustlsStream {
+ stream,
+ ssl_shutdown: true,
+ })
}
}
@@ -48,14 +52,17 @@ impl TlsConnector for RustlsConnector {
#[derive(Debug)]
pub struct RustlsStream {
stream: StreamOwned,
+ ssl_shutdown: bool,
}
impl TlsStream for RustlsStream {
type InnerStream = StreamOwned;
/// Get underlying tcp stream
- fn tcp_stream(self) -> TcpStream {
+ fn tcp_stream(mut self) -> TcpStream {
let mut stream = self.get_ref().try_clone().unwrap();
+ // Don't perform shutdown later
+ self.ssl_shutdown = false;
// flush stream (otherwise can cause bad chars on channel)
if let Err(err) = stream.flush() {
error!("Error in flushing tcp stream: {}", err);
@@ -74,3 +81,17 @@ impl TlsStream for RustlsStream {
&mut self.stream
}
}
+
+impl Drop for RustlsStream {
+ fn drop(&mut self) {
+ if self.ssl_shutdown {
+ if let Err(err) = self.stream.flush() {
+ error!("error in flushing rustls stream on drop: {err}");
+ }
+ self.stream.conn.send_close_notify();
+ if let Err(err) = self.stream.conn.write_tls(&mut self.stream.sock) {
+ error!("error in terminating rustls stream: {err}");
+ }
+ }
+ }
+}
diff --git a/suppaftp/src/test_container.rs b/suppaftp/src/test_container.rs
new file mode 100644
index 0000000..2d07cc5
--- /dev/null
+++ b/suppaftp/src/test_container.rs
@@ -0,0 +1,74 @@
+#![allow(dead_code)]
+
+use std::borrow::Cow;
+
+use testcontainers::core::WaitFor;
+use testcontainers::{Container, ContainerAsync, Image};
+
+#[derive(Debug, Default, Clone)]
+struct PureFtpImage {
+ _priv: (),
+}
+
+impl Image for PureFtpImage {
+ fn name(&self) -> &str {
+ "stilliard/pure-ftpd"
+ }
+
+ fn tag(&self) -> &str {
+ "latest"
+ }
+
+ fn ready_conditions(&self) -> Vec {
+ vec![WaitFor::message_on_stdout("Starting Pure-FTPd")]
+ }
+
+ fn env_vars(
+ &self,
+ ) -> impl IntoIterator- >, impl Into>)> {
+ vec![
+ ("PUBLICHOST", "localhost"),
+ ("FTP_USER_NAME", "test"),
+ ("FTP_USER_PASS", "test"),
+ ("FTP_USER_HOME", "/home/test"),
+ ]
+ }
+}
+
+pub struct AsyncPureFtpRunner {
+ container: ContainerAsync,
+}
+
+impl AsyncPureFtpRunner {
+ pub async fn start() -> Self {
+ use testcontainers::runners::AsyncRunner;
+ let container = PureFtpImage::default().start().await.unwrap();
+
+ Self { container }
+ }
+
+ pub async fn get_ftp_port(&self) -> u16 {
+ self.container.get_host_port_ipv4(21).await.unwrap()
+ }
+}
+
+pub struct SyncPureFtpRunner {
+ container: Container,
+}
+
+impl SyncPureFtpRunner {
+ pub fn start() -> Self {
+ use testcontainers::runners::SyncRunner;
+ let container = PureFtpImage::default().start().unwrap();
+
+ Self { container }
+ }
+
+ pub fn get_ftp_port(&self) -> u16 {
+ self.container.get_host_port_ipv4(21).unwrap()
+ }
+
+ pub fn get_mapped_port(&self, port: u16) -> u16 {
+ self.container.get_host_port_ipv4(port).unwrap()
+ }
+}
diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml
deleted file mode 100644
index b5e11a0..0000000
--- a/tests/docker-compose.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-version: "3"
-services:
- ftp-server:
- image: stilliard/pure-ftpd
- ports:
- - "10021:21"
- - "30000-30009:30000-30009"
- environment:
- - PUBLICHOST=localhost
- - FTP_USER_NAME=test
- - FTP_USER_PASS=test
- - FTP_USER_HOME=/home/test
diff --git a/tests/test.sh b/tests/test.sh
deleted file mode 100755
index ceefdfc..0000000
--- a/tests/test.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/usr/bin/env sh
-
-if [ ! -f docker-compose.yml ]; then
- set -e
- cd tests/
- set +e
-fi
-
-echo "Prepare volume..."
-rm -rf /tmp/suppaftp-test-ftp
-mkdir -p /tmp/suppaftp-test-ftp
-echo "Building docker image..."
-docker compose build
-set -e
-docker compose up -d
-set +e
-
-# Go back to src root
-cd ..
-# Run tests
-echo "Running tests"
-cargo test --features with-containers --features secure -- --test-threads 1
-TEST_RESULT=$?
-# Stop container
-cd tests/
-echo "Stopping container..."
-docker compose stop
-
-exit $TEST_RESULT