feat(web): per-device file tunnel for devicefile/uploadnodefile/uploadfilebatch (VS-3) #45

Merged
David merged 1 commit from feat/device-file-tunnel-vs-3 into main 2026-06-06 19:00:26 +02:00
Owner

Implements VS-3: one per-device file-tunnel allocator, and the three legacy HTTP file endpoints wired onto it.

Allocator

device_file::open_file_tunnel() follows the proven mcp_gateway pattern: stash one Side of an in-process relay pair in mesh_relays under a random file-<rand> id, mint a 60-second node-scoped rauth cookie, send msg/tunnel (usage=5, tunnel_mode::FILE_TRANSFER) over the agent's control WS with the *-prefixed relay URL, then wait for the rendezvous 'c' before returning. The agent's existing run_files loop answers; zero agent-side changes.

Endpoints (all with /:domain/ mirrors)

  • GET /devicefile.ashx?agent_id=<node>&path=<abs> (aliases nodeid/n, f): streams the file as an attachment download, one ack per 4-byte-header chunk, agent cancel maps to 404. No Content-Length; a mid-stream failure surfaces as a truncated chunked body.
  • POST /uploadnodefile.ashx?agent_id=<node>&path=<dir>: replays multipart file parts over one tunnel (upload/uploadstart/uploadack/uploaddone), applying the SPA's 0x00 escape for chunks starting with '{' or 0x00. Filenames are sanitized to their basename.
  • POST /uploadfilebatch.ashx?agent_ids=<a,b,c>&path=<dir> (alias nodeids): spools parts to temp files once (cleaned up on every return path via Drop), then pushes to each device over a fresh tunnel; per-node failures land in the JSON result map without aborting the batch.

Decisions (resolve the issue's open question)

  • Multipart variants reuse the agent's existing tunnel upload lifecycle; no dedicated multipart streaming protocol.
  • Rights gate: REMOTE_CONTROL on the node's mesh required, NO_FILES restriction denies, mesh ADMIN and full site-admins pass (upstream parity plus the file-restriction bit).

Acceptance criteria

  • All three routes return real bytes for a connected agent: integration-tested against a stub agent that mirrors vervain-agent's run_files (rendezvous handshake, protocol digit, end-bit chunk rule, escape strip).
  • Per-domain mirrors: routed and covered by download_works_via_per_domain_mirror.
  • End-to-end download test against the in-process stub agent: download_streams_file_end_to_end (40 bytes across three chunks, asserts headers, body, and zero orphaned rendezvous stashes).

Plus: two-node batch fan-out test, byte-for-byte upload assertion including the escape rule, and 401/403/503 gate tests. Upload routes carry a 1 GiB DefaultBodyLimit matching the agent's per-transfer ceiling. cargo test --workspace, cargo fmt --check, and cargo clippy --workspace --all-targets are clean.

Resolves VS-3.

Implements VS-3: one per-device file-tunnel allocator, and the three legacy HTTP file endpoints wired onto it. ## Allocator `device_file::open_file_tunnel()` follows the proven mcp_gateway pattern: stash one `Side` of an in-process relay pair in `mesh_relays` under a random `file-<rand>` id, mint a 60-second node-scoped `rauth` cookie, send `msg/tunnel` (`usage=5`, `tunnel_mode::FILE_TRANSFER`) over the agent's control WS with the `*`-prefixed relay URL, then wait for the rendezvous `'c'` before returning. The agent's existing `run_files` loop answers; zero agent-side changes. ## Endpoints (all with `/:domain/` mirrors) - `GET /devicefile.ashx?agent_id=<node>&path=<abs>` (aliases `nodeid`/`n`, `f`): streams the file as an `attachment` download, one ack per 4-byte-header chunk, agent `cancel` maps to 404. No Content-Length; a mid-stream failure surfaces as a truncated chunked body. - `POST /uploadnodefile.ashx?agent_id=<node>&path=<dir>`: replays multipart file parts over one tunnel (`upload`/`uploadstart`/`uploadack`/`uploaddone`), applying the SPA's `0x00` escape for chunks starting with `'{'` or `0x00`. Filenames are sanitized to their basename. - `POST /uploadfilebatch.ashx?agent_ids=<a,b,c>&path=<dir>` (alias `nodeids`): spools parts to temp files once (cleaned up on every return path via Drop), then pushes to each device over a fresh tunnel; per-node failures land in the JSON result map without aborting the batch. ## Decisions (resolve the issue's open question) - Multipart variants reuse the agent's existing tunnel upload lifecycle; no dedicated multipart streaming protocol. - Rights gate: `REMOTE_CONTROL` on the node's mesh required, `NO_FILES` restriction denies, mesh ADMIN and full site-admins pass (upstream parity plus the file-restriction bit). ## Acceptance criteria - All three routes return real bytes for a connected agent: integration-tested against a stub agent that mirrors vervain-agent's `run_files` (rendezvous handshake, protocol digit, end-bit chunk rule, escape strip). - Per-domain mirrors: routed and covered by `download_works_via_per_domain_mirror`. - End-to-end download test against the in-process stub agent: `download_streams_file_end_to_end` (40 bytes across three chunks, asserts headers, body, and zero orphaned rendezvous stashes). Plus: two-node batch fan-out test, byte-for-byte upload assertion including the escape rule, and 401/403/503 gate tests. Upload routes carry a 1 GiB `DefaultBodyLimit` matching the agent's per-transfer ceiling. `cargo test --workspace`, `cargo fmt --check`, and `cargo clippy --workspace --all-targets` are clean. Resolves VS-3.
feat(web): per-device file tunnel powers devicefile + upload ashx routes
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 1m34s
Create release / Create release from merged PR (pull_request) Has been skipped
375e53432c
Adds crates/meshcentral-web/src/device_file.rs: open_file_tunnel() stashes one side of an in-process relay pair in mesh_relays, mints a 60s node-scoped rauth cookie, sends msg/tunnel (usage=5, FILE_TRANSFER) over the agent's control WS, and waits for the rendezvous 'c' before handing back a FileTunnel that speaks the agent's run_files protocol through the meshrelay text/binary tag framing.

Built on it, three 501 hints become real handlers, each with a per-domain mirror:

- GET /devicefile.ashx?agent_id&path streams the file back as an attachment, one download ack per 4-byte-header chunk, agent open failures map to 404.
- POST /uploadnodefile.ashx?agent_id&path replays multipart file parts over one tunnel via upload/uploadstart/uploadack/uploaddone, applying the 0x00 escape for chunks starting with '{' or 0x00.
- POST /uploadfilebatch.ashx?agent_ids&path spools the parts to temp files once, then fans out to every listed device over a fresh tunnel each; per-node failures land in the result map instead of aborting the batch.

AuthZ per the VS-3 decision: REMOTE_CONTROL required on the node's mesh and the NO_FILES restriction denies; mesh ADMIN and site-admins pass. The multipart endpoints reuse the agent's existing tunnel upload lifecycle (the issue's open question, resolved with niceguyit) so no agent-side changes are needed. Upload routes get a 1 GiB DefaultBodyLimit matching the agent's per-transfer ceiling. meshrelay's frame tags become pub(crate) for the tunnel driver; reqwest dev-dep grows the multipart feature.

Integration tests stand up the full router with a stub agent that mirrors vervain-agent's run_files loop (rendezvous handshake, protocol digit, end-bit chunking, escape strip): download round-trip incl. per-domain mirror and 404 mapping, single and two-node multipart uploads byte-for-byte, plus 401/403/503 gates.

#VS-3
David merged commit 040dd21e58 into main 2026-06-06 19:00:26 +02:00
David deleted branch feat/device-file-tunnel-vs-3 2026-06-06 19:00:26 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-server!45
No description provided.