fix(daemon): correct the published contract and harden the control socket
A review pass over the diff found seventeen confirmed defects. The worst was
the contract itself: every method's parameters were published as a single
descriptor named `params`, so a client generated from openrpc.json sent
{"params":{"params":{…}}} and every call failed -32602. Parameters are now
published per field, with a test asserting the published names are the keys
the daemon actually deserializes.
Security and lifecycle:
- download.start no longer lets a caller name a directory, which was an
arbitrary write on the daemon's host.
- The Unix socket is bound under a temporary name and renamed into place; it
was briefly connectable by any local user between bind and chmod, and on
that transport connecting is authenticating.
- Pre-auth reads are capped at 1 MiB with a 30s handshake timeout, and
connections are capped, so an unauthenticated peer cannot exhaust memory or
threads.
- A read error no longer returns past the hub unsubscribe, which leaked a
subscriber and two threads per occurrence.
- A client whose event queue fills is now actually disconnected. It used to be
dropped from the fan-out and left connected, working on but blind to every
event after — the documentation already promised otherwise.
- A client whose daemon vanishes mid-transfer is told at once instead of
waiting out its full timeout and reporting a misleading "gave up".
Contract accuracy: errors carry an explicit null id (a missing one reads as a
notification and gets dropped), a second auth answers with the published
shape, and nullable fields are sent rather than omitted so a generated client
sees one shape.
Also splits proto.rs and server.rs, whose concerns had diverged, and applies
the review's simplifications.
This commit is contained in:
@@ -551,10 +551,8 @@ impl DaemonFixture {
|
||||
static COUNTER: std::sync::atomic::AtomicU32 =
|
||||
std::sync::atomic::AtomicU32::new(0);
|
||||
let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let state = std::env::temp_dir().join(format!(
|
||||
"soulseek-e2e-daemon-{}-{n}",
|
||||
std::process::id()
|
||||
));
|
||||
let state = std::env::temp_dir()
|
||||
.join(format!("soulseek-e2e-daemon-{}-{n}", std::process::id()));
|
||||
std::fs::create_dir_all(&state).expect("daemon state directory");
|
||||
|
||||
let mut args = vec![
|
||||
@@ -568,9 +566,10 @@ impl DaemonFixture {
|
||||
];
|
||||
// A listener of its own unless the test asked for something specific:
|
||||
// transfers arrive on a connection the uploader opens back to us.
|
||||
if !session_flags.iter().any(|flag| {
|
||||
flag == "--listener-port" || flag == "--no-listener"
|
||||
}) {
|
||||
if !session_flags
|
||||
.iter()
|
||||
.any(|flag| flag == "--listener-port" || flag == "--no-listener")
|
||||
{
|
||||
args.push("--listener-port".to_string());
|
||||
args.push(free_port().expect("listener port").to_string());
|
||||
}
|
||||
@@ -631,9 +630,7 @@ impl DaemonFixture {
|
||||
"daemon",
|
||||
"status",
|
||||
]);
|
||||
command
|
||||
.output()
|
||||
.is_ok_and(|output| output.status.success())
|
||||
command.output().is_ok_and(|output| output.status.success())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,9 +660,7 @@ impl TestServer {
|
||||
child: None,
|
||||
db: None,
|
||||
_gate: gate,
|
||||
daemons: std::sync::Mutex::new(
|
||||
std::collections::HashMap::new(),
|
||||
),
|
||||
daemons: std::sync::Mutex::new(std::collections::HashMap::new()),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user