mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-05 21:07:43 +00:00
5cda460451
Co-authored-by: houseme <housemecn@gmail.com>
227 lines
11 KiB
Rust
227 lines
11 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Public test entry points for the SFTP compliance suite.
|
|
//!
|
|
//! Three suite entries cover every CMPTST-NN identifier:
|
|
//!
|
|
//! - test_sftp_compliance_suite CMPTST-01..14 (one shared SFTP session)
|
|
//! - test_sftp_compliance_readonly CMPTST-15..23 (one shared session, read-only mode)
|
|
//! - test_sftp_compliance_standalone CMPTST-24..33 (each case spawns its own rustfs)
|
|
//!
|
|
//! The first two reuse a single rustfs spawn for the whole bracket
|
|
//! because every case in the bracket exercises the same protocol
|
|
//! against the same server. The third aggregates cases that each need
|
|
//! a different server configuration (idle timeout, read-cache window,
|
|
//! console disabled) and therefore cannot share a binary.
|
|
//!
|
|
//! Each per-case module exposes a single descriptive entry called
|
|
//! run_<what_it_tests>(). For example cmptst_24 exposes
|
|
//! cmptst_24::run_concurrent_half_close_no_leak().
|
|
//!
|
|
//! The per-case bodies (one cmptst_NN module per case) and the
|
|
//! cross-case infrastructure (spawn helpers, fixture seeders, the
|
|
//! half-close / wedge / paused-drain stream wrappers, and the session
|
|
//! lifecycle counters) live in sftp_compliance_tests.rs. Per-case
|
|
//! marker comments and the full case-index doc live there too.
|
|
|
|
use crate::protocols::sftp_compliance_tests::{
|
|
cmptst_01, cmptst_02, cmptst_03, cmptst_04, cmptst_05, cmptst_06, cmptst_07, cmptst_08, cmptst_09, cmptst_10, cmptst_11,
|
|
cmptst_12, cmptst_13, cmptst_14, cmptst_15, cmptst_16, cmptst_17, cmptst_18, cmptst_19, cmptst_20, cmptst_21, cmptst_22,
|
|
cmptst_23, cmptst_27, cmptst_28, cmptst_29, cmptst_32, cmptst_33, cmptst_34, spawn_compliance_rustfs,
|
|
};
|
|
#[cfg(target_os = "linux")]
|
|
use crate::protocols::sftp_compliance_tests::{cmptst_24, cmptst_25, cmptst_26};
|
|
use crate::protocols::sftp_helpers::{build_test_s3_client, connect_sftp_to, wait_for_s3_ready};
|
|
use crate::protocols::test_env::ProtocolTestEnvironment;
|
|
use anyhow::{Result, anyhow};
|
|
use aws_sdk_s3::primitives::ByteStream;
|
|
use tracing::info;
|
|
|
|
// Read-write compliance suite ports. Distinct from sftp_core (9022/9200)
|
|
// and from test_sftp_idle_timeout_disconnects (9023/9100) so the SFTP
|
|
// entries can run sequentially without leftover-listener contention.
|
|
const COMPLIANCE_RW_SFTP_PORT: u16 = 9024;
|
|
const COMPLIANCE_RW_SFTP_ADDRESS: &str = "127.0.0.1:9024";
|
|
const COMPLIANCE_RW_S3_ADDRESS: &str = "127.0.0.1:9300";
|
|
|
|
// Read-only compliance suite ports. The SFTP session opened against
|
|
// this address runs against a server started with
|
|
// RUSTFS_SFTP_READ_ONLY=true. The S3 endpoint stays writable so the
|
|
// suite can seed a bucket and a fixture object before running the SFTP
|
|
// rejection assertions.
|
|
const COMPLIANCE_RO_SFTP_PORT: u16 = 9025;
|
|
const COMPLIANCE_RO_SFTP_ADDRESS: &str = "127.0.0.1:9025";
|
|
const COMPLIANCE_RO_S3_ADDRESS: &str = "127.0.0.1:9301";
|
|
const COMPLIANCE_RO_S3_ENDPOINT: &str = "http://127.0.0.1:9301";
|
|
const COMPLIANCE_RO_S3_READY_ATTEMPTS: u32 = 30;
|
|
|
|
/// Compliance suite entry: spawn one rustfs server, run every per-case
|
|
/// helper that closes a coverage gap not exercised by sftp_core. Runs
|
|
/// CMPTST-01 through CMPTST-14 against the same SFTP session.
|
|
pub async fn test_sftp_compliance_suite() -> Result<()> {
|
|
info!("Starting SFTP server for compliance suite on {}", COMPLIANCE_RW_SFTP_ADDRESS);
|
|
let (_env, mut server_process) = spawn_compliance_rustfs(COMPLIANCE_RW_SFTP_ADDRESS, COMPLIANCE_RW_S3_ADDRESS, false).await?;
|
|
|
|
let result = async {
|
|
ProtocolTestEnvironment::wait_for_port_ready(COMPLIANCE_RW_SFTP_PORT, 30)
|
|
.await
|
|
.map_err(|e| anyhow!("{}", e))?;
|
|
|
|
let (session, sftp) = connect_sftp_to(COMPLIANCE_RW_SFTP_ADDRESS).await?;
|
|
|
|
cmptst_01::run_medium_binary_round_trip(&sftp).await?;
|
|
cmptst_02::run_zero_byte_round_trip(&sftp).await?;
|
|
cmptst_03::run_rm_on_bucket_path_rejected(&sftp).await?;
|
|
cmptst_04::run_rmdir_nonempty_bucket_rejected(&sftp).await?;
|
|
cmptst_05::run_rmdir_nonempty_subdir_rejected(&sftp).await?;
|
|
cmptst_06::run_path_traversal_get_rejected(&sftp).await?;
|
|
cmptst_07::run_dotdot_collapses_to_root(&sftp).await?;
|
|
cmptst_08::run_rename_cross_bucket(&sftp).await?;
|
|
cmptst_09::run_path_with_spaces_round_trip(&sftp).await?;
|
|
cmptst_10::run_readlink_rejected(&sftp).await?;
|
|
cmptst_11::run_setstat_after_put_returns_ok(&sftp).await?;
|
|
cmptst_12::run_rename_same_path_keeps_file(&sftp).await?;
|
|
cmptst_13::run_implicit_dir_round_trip(&sftp).await?;
|
|
cmptst_14::run_winscp_setstat_shape_on_handle(&sftp).await?;
|
|
|
|
// CMPTST-34 cross-checks the SFTP streaming-multipart write
|
|
// path against the S3 layer. The OPEN-time FileAttributes must
|
|
// reach the finalised object as x-amz-meta-* user metadata
|
|
// through the CreateMultipartUpload input field. The S3 client
|
|
// connects to the same rustfs process this suite already drives.
|
|
let s3 = build_test_s3_client(&format!("http://{COMPLIANCE_RW_S3_ADDRESS}"));
|
|
wait_for_s3_ready(&s3, 30).await?;
|
|
cmptst_34::run_open_attrs_round_trip_multipart(&sftp, &s3).await?;
|
|
|
|
drop(sftp);
|
|
session.disconnect(russh::Disconnect::ByApplication, "", "en").await?;
|
|
info!("SFTP compliance suite passed");
|
|
Ok::<(), anyhow::Error>(())
|
|
}
|
|
.await;
|
|
|
|
// Discard kill/wait errors on the teardown path: the test result
|
|
// above is the binding outcome, and a server that has already
|
|
// exited produces an error here that carries no useful signal.
|
|
server_process.kill_and_wait().await;
|
|
|
|
result
|
|
}
|
|
|
|
/// Read-only compliance entry: CMPTST-15 through CMPTST-23. The SFTP
|
|
/// server runs with RUSTFS_SFTP_READ_ONLY=true. Mutations through SFTP
|
|
/// must error. Reads through SFTP must succeed. The test seeds a bucket
|
|
/// and a file through the writable S3 endpoint before opening the SFTP
|
|
/// session.
|
|
pub async fn test_sftp_compliance_readonly() -> Result<()> {
|
|
info!("Starting SFTP server in read-only mode on {}", COMPLIANCE_RO_SFTP_ADDRESS);
|
|
let (_env, mut server_process) = spawn_compliance_rustfs(COMPLIANCE_RO_SFTP_ADDRESS, COMPLIANCE_RO_S3_ADDRESS, true).await?;
|
|
|
|
let result = async {
|
|
ProtocolTestEnvironment::wait_for_port_ready(COMPLIANCE_RO_SFTP_PORT, 30)
|
|
.await
|
|
.map_err(|e| anyhow!("{}", e))?;
|
|
|
|
let s3 = build_test_s3_client(COMPLIANCE_RO_S3_ENDPOINT);
|
|
wait_for_s3_ready(&s3, COMPLIANCE_RO_S3_READY_ATTEMPTS).await?;
|
|
|
|
let bucket = "robucket";
|
|
let seeded_key = "small.txt";
|
|
let seeded_content = b"read-only seed\n";
|
|
|
|
s3.create_bucket()
|
|
.bucket(bucket)
|
|
.send()
|
|
.await
|
|
.map_err(|e| anyhow!("S3 CreateBucket {} failed: {:?}", bucket, e))?;
|
|
s3.put_object()
|
|
.bucket(bucket)
|
|
.key(seeded_key)
|
|
.body(ByteStream::from_static(seeded_content))
|
|
.send()
|
|
.await
|
|
.map_err(|e| anyhow!("S3 PutObject {}/{} failed: {:?}", bucket, seeded_key, e))?;
|
|
info!("Seeded read-only fixture via S3: {}/{}", bucket, seeded_key);
|
|
|
|
let (session, sftp) = connect_sftp_to(COMPLIANCE_RO_SFTP_ADDRESS).await?;
|
|
|
|
cmptst_15::run_ro_put_rejected(&sftp, bucket).await?;
|
|
cmptst_16::run_ro_rm_rejected(&sftp, bucket, seeded_key).await?;
|
|
cmptst_17::run_ro_mkdir_rejected(&sftp).await?;
|
|
cmptst_18::run_ro_rmdir_rejected(&sftp, bucket).await?;
|
|
cmptst_19::run_ro_rename_rejected(&sftp, bucket, seeded_key).await?;
|
|
cmptst_20::run_ro_ls_allowed(&sftp, bucket).await?;
|
|
cmptst_21::run_ro_get_allowed(&sftp, bucket, seeded_key, seeded_content).await?;
|
|
cmptst_22::run_ro_setstat_rejected(&sftp, bucket, seeded_key).await?;
|
|
cmptst_23::run_ro_fsetstat_rejected(&sftp, bucket, seeded_key).await?;
|
|
|
|
drop(sftp);
|
|
// Discard the disconnect Result. A read-only session that has
|
|
// returned errors against every mutation can still be cleanly
|
|
// torn down, but a transient transport-level error here
|
|
// carries no useful signal beyond what the assertions above
|
|
// already pin.
|
|
let _ = session.disconnect(russh::Disconnect::ByApplication, "", "en").await;
|
|
info!("SFTP read-only compliance suite passed");
|
|
Ok::<(), anyhow::Error>(())
|
|
}
|
|
.await;
|
|
|
|
server_process.kill_and_wait().await;
|
|
|
|
result
|
|
}
|
|
|
|
/// Standalone-server compliance entry: runs CMPTST-24..33 in numerical
|
|
/// order. Each case spawns and tears down its own rustfs because each
|
|
/// exercises a different server configuration (idle timeout, console
|
|
/// listener, read-cache window) that cannot share a process with the
|
|
/// others.
|
|
///
|
|
/// CMPTST-30 is omitted by default. Its assertion (per-operation
|
|
/// wall-clock latency under pipelined metadata ops) is bounded by the
|
|
/// SSH SFTP subsystem's per-channel serial handler dispatch and is
|
|
/// structurally infeasible against the production code. The
|
|
/// test_sftp_handler_latency_regression #[tokio::test] entry remains
|
|
/// runnable on demand via `--ignored`.
|
|
///
|
|
/// CMPTST-31 (paused-drain) is omitted from the default suite for
|
|
/// runtime cost (200 MiB seed plus a 25 s pause window). The
|
|
/// test_sftp_paused_drain_regression #[tokio::test] entry covers it
|
|
/// for direct invocation.
|
|
///
|
|
/// CMPTST-24, 25, 26 verify kernel-level state via ss(8) and the
|
|
/// procfs ESTABLISHED discriminator. They are skipped on non-Linux
|
|
/// targets where those interfaces are absent.
|
|
pub async fn test_sftp_compliance_standalone() -> Result<()> {
|
|
info!("Starting SFTP standalone-server compliance suite");
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
cmptst_24::run_concurrent_half_close_no_leak().await?;
|
|
cmptst_25::run_wedge_kill_after_silence_in_close_wait().await?;
|
|
cmptst_26::run_healthy_idle_session_above_fast_threshold().await?;
|
|
}
|
|
|
|
cmptst_27::run_multi_session_mixed_pipelining().await?;
|
|
cmptst_28::run_5mb_download_with_concurrent_metadata_ops().await?;
|
|
cmptst_29::run_read_past_eof_volume().await?;
|
|
cmptst_32::run_read_cache_enabled_round_trip().await?;
|
|
cmptst_33::run_read_cache_disabled_round_trip().await?;
|
|
|
|
info!("SFTP standalone-server compliance suite passed");
|
|
Ok(())
|
|
}
|