Randomize FTP upload stages
This commit is contained in:
Generated
+47
-1
@@ -14,6 +14,12 @@ version = "2.13.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -26,6 +32,18 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
@@ -71,6 +89,12 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.189"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.34"
|
version = "0.4.34"
|
||||||
@@ -98,6 +122,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|||||||
name = "memona-plugin-ftp"
|
name = "memona-plugin-ftp"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
"memona-plugin-sdk",
|
"memona-plugin-sdk",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@@ -106,7 +131,7 @@ dependencies = [
|
|||||||
name = "memona-plugin-sdk"
|
name = "memona-plugin-sdk"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wit-bindgen",
|
"wit-bindgen 0.62.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -143,6 +168,12 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.28"
|
version = "1.0.28"
|
||||||
@@ -229,6 +260,15 @@ version = "1.0.26"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.1+wasi-0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen 0.46.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-encoder"
|
name = "wasm-encoder"
|
||||||
version = "0.259.0"
|
version = "0.259.0"
|
||||||
@@ -263,6 +303,12 @@ dependencies = [
|
|||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.46.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen"
|
name = "wit-bindgen"
|
||||||
version = "0.62.0"
|
version = "0.62.0"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ license = "MIT"
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
getrandom = "0.3"
|
||||||
memona-plugin-sdk = { path = "sdk" }
|
memona-plugin-sdk = { path = "sdk" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
+19
-7
@@ -32,7 +32,6 @@ const MAX_STAGE_ATTEMPTS: usize = 64;
|
|||||||
thread_local! {
|
thread_local! {
|
||||||
static CONTROL: RefCell<Option<Control>> = const { RefCell::new(None) };
|
static CONTROL: RefCell<Option<Control>> = const { RefCell::new(None) };
|
||||||
static SESSION: RefCell<Option<Session>> = const { RefCell::new(None) };
|
static SESSION: RefCell<Option<Session>> = const { RefCell::new(None) };
|
||||||
static NEXT_STAGE: Cell<u64> = const { Cell::new(0) };
|
|
||||||
/// Raw MLSD listings by path, so later pages of a folder are parsed from
|
/// Raw MLSD listings by path, so later pages of a folder are parsed from
|
||||||
/// memory instead of downloaded and parsed whole again.
|
/// memory instead of downloaded and parsed whole again.
|
||||||
static LISTINGS: RefCell<HashMap<String, Rc<[u8]>>> = RefCell::new(HashMap::new());
|
static LISTINGS: RefCell<HashMap<String, Rc<[u8]>>> = RefCell::new(HashMap::new());
|
||||||
@@ -366,18 +365,27 @@ impl Drop for FtpWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Chooses an unused private sibling for one upload. The sequence is local to
|
fn random_stage_token() -> Result<u128, ErrorCode> {
|
||||||
/// the component, and the remote probe prevents reuse after an app restart.
|
let mut bytes = [0; 16];
|
||||||
fn stage_path(target: &str) -> Result<String, ErrorCode> {
|
getrandom::fill(&mut bytes).map_err(|_| ErrorCode::Internal)?;
|
||||||
|
Ok(u128::from_be_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Chooses an unused, unguessable private sibling for one upload. The remote
|
||||||
|
/// probe remains a collision check, while 128 random bits prevent another
|
||||||
|
/// client from racing a predictable name between that probe and `STOR`.
|
||||||
|
fn stage_path_with(
|
||||||
|
target: &str,
|
||||||
|
mut next_token: impl FnMut() -> Result<u128, ErrorCode>,
|
||||||
|
) -> Result<String, ErrorCode> {
|
||||||
let (parent, name) =
|
let (parent, name) =
|
||||||
target.rsplit_once('/').ok_or(ErrorCode::InvalidInput)?;
|
target.rsplit_once('/').ok_or(ErrorCode::InvalidInput)?;
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
return Err(ErrorCode::InvalidInput);
|
return Err(ErrorCode::InvalidInput);
|
||||||
}
|
}
|
||||||
for _ in 0..MAX_STAGE_ATTEMPTS {
|
for _ in 0..MAX_STAGE_ATTEMPTS {
|
||||||
let sequence = NEXT_STAGE.get();
|
let token = next_token()?;
|
||||||
NEXT_STAGE.set(sequence.wrapping_add(1));
|
let staged = format!("{parent}/.memona-upload-{token:032x}");
|
||||||
let staged = format!("{parent}/.memona-upload-{sequence:016x}");
|
|
||||||
match with_control(|control| probe(control, &staged)) {
|
match with_control(|control| probe(control, &staged)) {
|
||||||
Err(ErrorCode::NotFound) => return Ok(staged),
|
Err(ErrorCode::NotFound) => return Ok(staged),
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
@@ -387,6 +395,10 @@ fn stage_path(target: &str) -> Result<String, ErrorCode> {
|
|||||||
Err(ErrorCode::Conflict)
|
Err(ErrorCode::Conflict)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stage_path(target: &str) -> Result<String, ErrorCode> {
|
||||||
|
stage_path_with(target, random_stage_token)
|
||||||
|
}
|
||||||
|
|
||||||
fn start_writer(target: String) -> Result<FtpWriter, ErrorCode> {
|
fn start_writer(target: String) -> Result<FtpWriter, ErrorCode> {
|
||||||
ready()?;
|
ready()?;
|
||||||
let staged = stage_path(&target)?;
|
let staged = stage_path(&target)?;
|
||||||
|
|||||||
+15
-1
@@ -1,7 +1,7 @@
|
|||||||
//! FTP command recovery against an in-memory protocol transport. Live QA
|
//! FTP command recovery against an in-memory protocol transport. Live QA
|
||||||
//! separately exercises the compiled WASM component over real host sockets.
|
//! separately exercises the compiled WASM component over real host sockets.
|
||||||
|
|
||||||
use super::{Ftp, start_writer, test_host};
|
use super::{Ftp, stage_path_with, start_writer, test_host};
|
||||||
use memona_plugin_sdk::{ErrorCode, Guest, GuestWriter, Locator, Setting};
|
use memona_plugin_sdk::{ErrorCode, Guest, GuestWriter, Locator, Setting};
|
||||||
|
|
||||||
type TestResult<T = ()> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
type TestResult<T = ()> = Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||||
@@ -157,3 +157,17 @@ async fn dropping_an_upload_removes_only_its_private_stage() -> TestResult {
|
|||||||
assert_eq!(summary.promotions, 0);
|
assert_eq!(summary.promotions, 0);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_stage_name_collision_draws_a_fresh_random_token() -> TestResult {
|
||||||
|
let fixture = test_host::install(test_host::Scenario::StageCollision);
|
||||||
|
guest(Ftp::open("ftp".into(), config()))?;
|
||||||
|
let mut tokens = [1, 2].into_iter();
|
||||||
|
let staged = guest(stage_path_with("/target", || {
|
||||||
|
tokens.next().ok_or(ErrorCode::Internal)
|
||||||
|
}))?;
|
||||||
|
assert_eq!(staged, "/.memona-upload-00000000000000000000000000000002");
|
||||||
|
Ftp::close();
|
||||||
|
assert_eq!(fixture.summary().stage_probes, 2);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub enum Scenario {
|
|||||||
LostUploadData,
|
LostUploadData,
|
||||||
LostMutationReply,
|
LostMutationReply,
|
||||||
LostUploadCompletion,
|
LostUploadCompletion,
|
||||||
|
StageCollision,
|
||||||
SuccessfulUpload,
|
SuccessfulUpload,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ struct State {
|
|||||||
target_upload_commands: usize,
|
target_upload_commands: usize,
|
||||||
uploaded_bytes: usize,
|
uploaded_bytes: usize,
|
||||||
stage_deletes: usize,
|
stage_deletes: usize,
|
||||||
|
stage_probes: usize,
|
||||||
promotions: usize,
|
promotions: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ pub struct Summary {
|
|||||||
pub target_upload_commands: usize,
|
pub target_upload_commands: usize,
|
||||||
pub uploaded_bytes: usize,
|
pub uploaded_bytes: usize,
|
||||||
pub stage_deletes: usize,
|
pub stage_deletes: usize,
|
||||||
|
pub stage_probes: usize,
|
||||||
pub promotions: usize,
|
pub promotions: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +64,7 @@ pub fn install(scenario: Scenario) -> Fixture {
|
|||||||
target_upload_commands: 0,
|
target_upload_commands: 0,
|
||||||
uploaded_bytes: 0,
|
uploaded_bytes: 0,
|
||||||
stage_deletes: 0,
|
stage_deletes: 0,
|
||||||
|
stage_probes: 0,
|
||||||
promotions: 0,
|
promotions: 0,
|
||||||
}));
|
}));
|
||||||
ACTIVE.set(Some(state.clone()));
|
ACTIVE.set(Some(state.clone()));
|
||||||
@@ -78,6 +82,7 @@ impl Fixture {
|
|||||||
target_upload_commands: state.target_upload_commands,
|
target_upload_commands: state.target_upload_commands,
|
||||||
uploaded_bytes: state.uploaded_bytes,
|
uploaded_bytes: state.uploaded_bytes,
|
||||||
stage_deletes: state.stage_deletes,
|
stage_deletes: state.stage_deletes,
|
||||||
|
stage_probes: state.stage_probes,
|
||||||
promotions: state.promotions,
|
promotions: state.promotions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,7 +201,14 @@ impl Tcp {
|
|||||||
return Err(HostError::Unavailable);
|
return Err(HostError::Unavailable);
|
||||||
}
|
}
|
||||||
"NOOP" => "200 Still connected\r\n",
|
"NOOP" => "200 Still connected\r\n",
|
||||||
|
"MLST /.memona-upload-00000000000000000000000000000001"
|
||||||
|
if matches!(state.scenario, Scenario::StageCollision) =>
|
||||||
|
{
|
||||||
|
state.stage_probes += 1;
|
||||||
|
"250-Begin\r\n type=file;size=3; /.memona-upload-00000000000000000000000000000001\r\n250 End\r\n"
|
||||||
|
}
|
||||||
command if command.starts_with("MLST /.memona-upload-") => {
|
command if command.starts_with("MLST /.memona-upload-") => {
|
||||||
|
state.stage_probes += 1;
|
||||||
"550 Missing\r\n"
|
"550 Missing\r\n"
|
||||||
}
|
}
|
||||||
command
|
command
|
||||||
|
|||||||
Reference in New Issue
Block a user