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"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
@@ -26,6 +32,18 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
@@ -71,6 +89,12 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
@@ -98,6 +122,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
name = "memona-plugin-ftp"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"memona-plugin-sdk",
|
||||
"tokio",
|
||||
]
|
||||
@@ -106,7 +131,7 @@ dependencies = [
|
||||
name = "memona-plugin-sdk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
"wit-bindgen 0.62.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -143,6 +168,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
@@ -229,6 +260,15 @@ version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.259.0"
|
||||
@@ -263,6 +303,12 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.62.0"
|
||||
|
||||
@@ -8,6 +8,7 @@ license = "MIT"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
getrandom = "0.3"
|
||||
memona-plugin-sdk = { path = "sdk" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
+19
-7
@@ -32,7 +32,6 @@ const MAX_STAGE_ATTEMPTS: usize = 64;
|
||||
thread_local! {
|
||||
static CONTROL: RefCell<Option<Control>> = 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
|
||||
/// memory instead of downloaded and parsed whole again.
|
||||
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
|
||||
/// the component, and the remote probe prevents reuse after an app restart.
|
||||
fn stage_path(target: &str) -> Result<String, ErrorCode> {
|
||||
fn random_stage_token() -> Result<u128, ErrorCode> {
|
||||
let mut bytes = [0; 16];
|
||||
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) =
|
||||
target.rsplit_once('/').ok_or(ErrorCode::InvalidInput)?;
|
||||
if name.is_empty() {
|
||||
return Err(ErrorCode::InvalidInput);
|
||||
}
|
||||
for _ in 0..MAX_STAGE_ATTEMPTS {
|
||||
let sequence = NEXT_STAGE.get();
|
||||
NEXT_STAGE.set(sequence.wrapping_add(1));
|
||||
let staged = format!("{parent}/.memona-upload-{sequence:016x}");
|
||||
let token = next_token()?;
|
||||
let staged = format!("{parent}/.memona-upload-{token:032x}");
|
||||
match with_control(|control| probe(control, &staged)) {
|
||||
Err(ErrorCode::NotFound) => return Ok(staged),
|
||||
Ok(_) => {}
|
||||
@@ -387,6 +395,10 @@ fn stage_path(target: &str) -> Result<String, ErrorCode> {
|
||||
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> {
|
||||
ready()?;
|
||||
let staged = stage_path(&target)?;
|
||||
|
||||
+15
-1
@@ -1,7 +1,7 @@
|
||||
//! FTP command recovery against an in-memory protocol transport. Live QA
|
||||
//! 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};
|
||||
|
||||
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);
|
||||
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,
|
||||
LostMutationReply,
|
||||
LostUploadCompletion,
|
||||
StageCollision,
|
||||
SuccessfulUpload,
|
||||
}
|
||||
|
||||
@@ -31,6 +32,7 @@ struct State {
|
||||
target_upload_commands: usize,
|
||||
uploaded_bytes: usize,
|
||||
stage_deletes: usize,
|
||||
stage_probes: usize,
|
||||
promotions: usize,
|
||||
}
|
||||
|
||||
@@ -48,6 +50,7 @@ pub struct Summary {
|
||||
pub target_upload_commands: usize,
|
||||
pub uploaded_bytes: usize,
|
||||
pub stage_deletes: usize,
|
||||
pub stage_probes: usize,
|
||||
pub promotions: usize,
|
||||
}
|
||||
|
||||
@@ -61,6 +64,7 @@ pub fn install(scenario: Scenario) -> Fixture {
|
||||
target_upload_commands: 0,
|
||||
uploaded_bytes: 0,
|
||||
stage_deletes: 0,
|
||||
stage_probes: 0,
|
||||
promotions: 0,
|
||||
}));
|
||||
ACTIVE.set(Some(state.clone()));
|
||||
@@ -78,6 +82,7 @@ impl Fixture {
|
||||
target_upload_commands: state.target_upload_commands,
|
||||
uploaded_bytes: state.uploaded_bytes,
|
||||
stage_deletes: state.stage_deletes,
|
||||
stage_probes: state.stage_probes,
|
||||
promotions: state.promotions,
|
||||
}
|
||||
}
|
||||
@@ -196,7 +201,14 @@ impl Tcp {
|
||||
return Err(HostError::Unavailable);
|
||||
}
|
||||
"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-") => {
|
||||
state.stage_probes += 1;
|
||||
"550 Missing\r\n"
|
||||
}
|
||||
command
|
||||
|
||||
Reference in New Issue
Block a user