feat(hh): P5 — file & directory uploads ⛧
Wire-compatible with the Python client's _ft protocol (offer/accept/reject/ chunk/done, 64KB chunks, SHA-256 verified), over the encrypted channel: - ft.rs: read_payload (file or tar'd directory), save/extract with a zip-slip guard (relative-only, no .. / absolute; unpack_in double-checks), SHA-256. - /send <file> and /sendd <dir>; receiver sees an offer banner, /accept or /reject; chunks stream in the background and the result is verified + saved to ./downloads (directories extracted in place). - Refactor: all outgoing ws frames now funnel through an mpsc channel drained (batched) by the run loop, so the background chunk-sender and the PTY relay transmit without owning the socket. - ui.rs: pending-offer banner on the input bar. Tests: 7 cargo tests (file + dir tar round-trip, traversal guard, + crypto/sbx). Verified live: two TUIs, file and directory transfer, SHA-256 verified, saved. Note: dropping accepted files into the active sandbox VM dir is a future add-on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+10
-6
@@ -120,7 +120,7 @@ enum Decoded {
|
||||
}
|
||||
|
||||
/// Decrypt + classify one stored/broadcast message object.
|
||||
fn decode_msg(room: &fernet::Fernet, m: &Value, allow_sbx: bool) -> Decoded {
|
||||
fn decode_msg(room: &fernet::Fernet, m: &Value, live: bool) -> Decoded {
|
||||
let ct = match m["text"].as_str() {
|
||||
Some(c) if !c.is_empty() => c,
|
||||
_ => return Decoded::Skip,
|
||||
@@ -128,22 +128,26 @@ fn decode_msg(room: &fernet::Fernet, m: &Value, allow_sbx: bool) -> Decoded {
|
||||
let (text, system) = match room.decrypt(ct) {
|
||||
Ok(pt) => {
|
||||
let t = String::from_utf8_lossy(&pt).to_string();
|
||||
if t.starts_with("{\"_ft\":") {
|
||||
return Decoded::Skip; // file-transfer control frame (P5)
|
||||
}
|
||||
// Server-stamped (authenticated) sender of this message.
|
||||
let sender = m["username"].as_str().unwrap_or("?");
|
||||
if t.starts_with("{\"_perm\":") {
|
||||
return parse_perm(&t).map(Decoded::Sbx).unwrap_or(Decoded::Skip);
|
||||
}
|
||||
// Control frames are live-only — never replayed from the stored snapshot.
|
||||
if t.starts_with("{\"_sbx\":") {
|
||||
// Don't replay terminal history from the stored snapshot.
|
||||
return if allow_sbx {
|
||||
return if live {
|
||||
parse_sbx(&t, sender).map(Decoded::Sbx).unwrap_or(Decoded::Skip)
|
||||
} else {
|
||||
Decoded::Skip
|
||||
};
|
||||
}
|
||||
if t.starts_with("{\"_ft\":") {
|
||||
return if live {
|
||||
crate::ft::parse(&t, sender).map(|f| Decoded::Sbx(Net::Ft(f))).unwrap_or(Decoded::Skip)
|
||||
} else {
|
||||
Decoded::Skip
|
||||
};
|
||||
}
|
||||
(t, false)
|
||||
}
|
||||
Err(_) => ("[unreadable — wrong room password?]".to_string(), true),
|
||||
|
||||
Reference in New Issue
Block a user