Skip to main content

netget/server/rsync/
mod.rs

1//! rsync daemon (rsync://, TCP 873) at protocol 29. Rust owns the handshake, the file-list
2//! encoding and order, the transfer tokens, the checksums and the end-of-run dance; the model
3//! says which modules exist, what each requested path holds, and which uploaded files to take
4//! (their contents are handed to it, never written to disk).
5pub mod actions;
6pub mod wire;
7
8use crate::llm::action_helper::call_llm;
9use crate::llm::actions::protocol_trait::ActionResult;
10use crate::logging::emit::Log;
11use crate::protocol::{Event, SpawnContext};
12use crate::server::connection::ConnectionId;
13use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
14use anyhow::{bail, ensure, Context, Result};
15use serde_json::{json, Value};
16use std::net::SocketAddr;
17use std::time::Duration;
18use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
19use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
20use tokio::net::{TcpListener, TcpStream};
21use wire::{Entry, Kind, Request};
22
23/// Deadline for each line or request from the client.
24pub const IDLE_TIMEOUT: Duration = Duration::from_secs(120);
25pub const MAX_IDLE_TIMEOUT_SECS: u64 = 3600;
26/// Paths one request may name.
27pub const MAX_PATHS: usize = 16;
28const WRITE_TIMEOUT: Duration = Duration::from_secs(60);
29/// Largest single file an upload may deliver; a bigger one is refused before it is requested.
30pub const MAX_UPLOAD_FILE_BYTES: u64 = 1024 * 1024;
31/// All the files of one upload together: the declared sizes of the files taken, and the file
32/// data actually received (counted as it arrives, including data past a file's declared size).
33pub const MAX_UPLOAD_BYTES: u64 = 8 * 1024 * 1024;
34/// The largest message one connection buffers from a client: an uploaded file, or the names
35/// and symlink targets of an upload's file list, whichever bound is higher.
36pub const MAX_INBOUND_BYTES: usize =
37    if MAX_UPLOAD_FILE_BYTES as usize > wire::MAX_UPLOAD_LIST_NAME_BYTES {
38        MAX_UPLOAD_FILE_BYTES as usize
39    } else {
40        wire::MAX_UPLOAD_LIST_NAME_BYTES
41    };
42/// Longest text one multiplexed message carries, in bytes (cut on a character boundary).
43pub const MAX_MESSAGE_BYTES: usize = 4000;
44/// Files one upload may deliver.
45pub const MAX_UPLOAD_FILES: usize = 1000;
46
47#[derive(Clone)]
48struct Config {
49    motd: Option<String>,
50    idle: Duration,
51}
52
53pub async fn spawn(ctx: SpawnContext) -> Result<SocketAddr> {
54    let params = ctx.startup_params.as_ref();
55    let motd = params
56        .map(|p| p.get_optional_string("motd"))
57        .transpose()?
58        .flatten();
59    let idle = params
60        .map(|p| p.get_optional_u64("idle_timeout_secs"))
61        .transpose()?
62        .flatten()
63        .unwrap_or(IDLE_TIMEOUT.as_secs());
64    ensure!(
65        (1..=MAX_IDLE_TIMEOUT_SECS).contains(&idle),
66        "idle_timeout_secs must be between 1 and {MAX_IDLE_TIMEOUT_SECS}"
67    );
68    let cfg = Config {
69        motd: motd.map(|m| crate::utils::sanitize::multiline(&m)),
70        idle: Duration::from_secs(idle),
71    };
72    let listener = TcpListener::bind(ctx.legacy_listen_addr()).await?;
73    let addr = listener.local_addr()?;
74    Log::new(Some(&ctx.status_tx)).info(format!("rsync daemon listening on {addr} (protocol 29)"));
75    let state = ctx.state.clone();
76    let server_id = ctx.server_id;
77    let accept = tokio::spawn(async move {
78        let limiter = crate::server::accept_bounded::ConnectionLimiter::new(
79            crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS,
80        );
81        loop {
82            let (socket, peer, permit) = match crate::server::accept_bounded::accept_bounded(
83                &listener,
84                &limiter,
85                b"@ERROR: max connections reached -- try again later\n",
86                "rsync",
87                Some(&ctx.status_tx),
88            )
89            .await
90            {
91                Ok(v) => v,
92                Err(_) => break,
93            };
94            let id = ConnectionId::new(ctx.state.get_next_unified_id().await);
95            let now = crate::utils::clock::Instant::now();
96            ctx.state
97                .add_connection_to_server(
98                    server_id,
99                    ConnectionState {
100                        id,
101                        remote_addr: peer,
102                        local_addr: addr,
103                        bytes_sent: 0,
104                        bytes_received: 0,
105                        packets_sent: 0,
106                        packets_received: 0,
107                        last_activity: now,
108                        status: ConnectionStatus::Active,
109                        status_changed_at: now,
110                        protocol_info: ProtocolConnectionInfo::empty(),
111                    },
112                )
113                .await;
114            let child = ctx.clone();
115            let cfg = cfg.clone();
116            ctx.state
117                .spawn_server_task(server_id, async move {
118                    let _permit = permit;
119                    if let Err(e) = session(&child, id, socket, peer, &cfg).await {
120                        Log::new(Some(&child.status_tx))
121                            .warn(format!("rsync connection {id} ended: {e:#}"));
122                    }
123                    child
124                        .state
125                        .update_connection_status(server_id, id, ConnectionStatus::Closed)
126                        .await;
127                    let _ = child.status_tx.send("__UPDATE_UI__".into());
128                })
129                .await;
130        }
131    });
132    state.register_server_task(server_id, accept).await;
133    Ok(addr)
134}
135
136/// The client side of the socket: raw at protocol 29, with a read deadline and a byte count.
137struct In {
138    rd: BufReader<OwnedReadHalf>,
139    idle: Duration,
140    total: u64,
141}
142
143impl In {
144    async fn exact(&mut self, n: usize) -> Result<Vec<u8>> {
145        let mut buf = vec![0u8; n];
146        tokio::time::timeout(self.idle, self.rd.read_exact(&mut buf))
147            .await
148            .context("the client went quiet")??;
149        self.total += n as u64;
150        Ok(buf)
151    }
152    async fn int(&mut self) -> Result<i32> {
153        let b = self.exact(4).await?;
154        Ok(i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
155    }
156    async fn short(&mut self) -> Result<u16> {
157        let b = self.exact(2).await?;
158        Ok(u16::from_le_bytes([b[0], b[1]]))
159    }
160    /// One `\n`-terminated line, `\r` dropped, at most `MAX_LINE` bytes.
161    async fn line(&mut self) -> Result<String> {
162        let mut out = Vec::new();
163        loop {
164            let b = self.exact(1).await?[0];
165            match b {
166                b'\n' => break,
167                b'\r' => {}
168                0 => bail!("a NUL byte in a text line"),
169                _ => {
170                    ensure!(
171                        out.len() < wire::MAX_LINE,
172                        "a line longer than {} bytes",
173                        wire::MAX_LINE
174                    );
175                    out.push(b);
176                }
177            }
178        }
179        Ok(String::from_utf8_lossy(&out).into_owned())
180    }
181    /// Read and discard `n` bytes.
182    async fn skip(&mut self, mut n: u64) -> Result<()> {
183        while n > 0 {
184            let k = n.min(64 * 1024) as usize;
185            self.exact(k).await?;
186            n -= k as u64;
187        }
188        Ok(())
189    }
190}
191
192struct Out {
193    wr: OwnedWriteHalf,
194    total: u64,
195}
196
197impl Out {
198    async fn raw(&mut self, bytes: &[u8]) -> Result<()> {
199        tokio::time::timeout(WRITE_TIMEOUT, self.wr.write_all(bytes))
200            .await
201            .context("rsync write deadline")??;
202        self.total += bytes.len() as u64;
203        Ok(())
204    }
205    async fn data(&mut self, bytes: &[u8]) -> Result<()> {
206        self.raw(&wire::data_frames(bytes)).await
207    }
208    async fn message(&mut self, code: u8, text: &str) -> Result<()> {
209        self.raw(&wire::frame(code, message_text(text).as_bytes()))
210            .await
211    }
212}
213
214/// The text of a message to the client: control characters removed, cut to at most
215/// `MAX_MESSAGE_BYTES` on a character boundary (a client-chosen path can put a multi-byte
216/// character across the cut), and ending in a newline.
217pub fn message_text(text: &str) -> String {
218    let clean = crate::utils::sanitize::multiline(text);
219    let mut t = crate::utils::truncate::truncate_str(&clean, MAX_MESSAGE_BYTES).to_string();
220    if !t.ends_with('\n') {
221        t.push('\n');
222    }
223    t
224}
225
226/// Close our side after a refusal, then let the client's bytes drain (for at most 5 s) so the
227/// error just sent is not lost to a reset.
228async fn close_and_drain(input: &mut In, out: &mut Out) {
229    let _ = out.wr.shutdown().await;
230    let _ = tokio::time::timeout(Duration::from_secs(5), async {
231        let mut sink = [0u8; 4096];
232        while matches!(input.rd.read(&mut sink).await, Ok(n) if n > 0) {}
233    })
234    .await;
235}
236
237fn now_secs() -> u32 {
238    crate::utils::clock::SystemTime::now()
239        .duration_since(crate::utils::clock::UNIX_EPOCH)
240        .map(|d| d.as_secs() as u32)
241        .unwrap_or(0)
242}
243
244async fn ask(
245    ctx: &SpawnContext,
246    id: ConnectionId,
247    event: Event,
248) -> Result<Option<(String, Value)>> {
249    let execution = call_llm(
250        &ctx.llm_client,
251        &ctx.state,
252        ctx.server_id,
253        Some(id),
254        &event,
255        &actions::RsyncProtocol,
256    )
257    .await?;
258    Ok(execution.protocol_results.iter().find_map(|r| match r {
259        ActionResult::Custom { name, data } => Some((name.clone(), data.clone())),
260        _ => None,
261    }))
262}
263
264fn decision(ctx: &SpawnContext, id: ConnectionId, operation: &str, decision: &str) {
265    let line = format!("rsync connection {id} operation={operation} decision={decision}");
266    let log = Log::new(Some(&ctx.status_tx));
267    if decision.starts_with("fail_closed") {
268        log.error(line);
269    } else {
270        log.info(line);
271    }
272}
273
274async fn session(
275    ctx: &SpawnContext,
276    id: ConnectionId,
277    socket: TcpStream,
278    peer: SocketAddr,
279    cfg: &Config,
280) -> Result<()> {
281    let (rd, wr) = socket.into_split();
282    let mut input = In {
283        rd: BufReader::new(rd),
284        idle: cfg.idle,
285        total: 0,
286    };
287    let mut out = Out { wr, total: 0 };
288    let mut hello = wire::GREETING.to_string();
289    if let Some(motd) = &cfg.motd {
290        hello.push_str(motd);
291        hello.push('\n');
292    }
293    out.raw(hello.as_bytes()).await?;
294    let greeting = input.line().await?;
295    let major: i32 = greeting
296        .strip_prefix("@RSYNCD: ")
297        .and_then(|v| v.split(['.', ' ']).next())
298        .and_then(|v| v.parse().ok())
299        .unwrap_or(0);
300    if major < wire::PROTOCOL {
301        out.raw(b"@ERROR: protocol startup error\n").await?;
302        bail!("client greeting {greeting:?}: protocol 29 or later is required");
303    }
304    let module = input.line().await?;
305    let client = peer.to_string();
306    if module.is_empty() || module == "#list" {
307        return list_modules(ctx, id, &mut out, &client).await;
308    }
309    if module.starts_with('#') || module.contains('/') {
310        out.raw(b"@ERROR: request not supported by this daemon\n")
311            .await?;
312        return Ok(());
313    }
314    out.raw(b"@RSYNCD: OK\n").await?;
315    let mut args = Vec::new();
316    loop {
317        let a = input.line().await?;
318        if a.is_empty() {
319            break;
320        }
321        ensure!(
322            args.len() < wire::MAX_ARGS,
323            "more than {} arguments",
324            wire::MAX_ARGS
325        );
326        args.push(a);
327    }
328    let req = wire::parse_args(&args, &module);
329    let seed = req.checksum_seed.unwrap_or_else(|| loop {
330        let s: i32 = rand::random();
331        if s != 0 {
332            break s;
333        }
334    });
335    out.raw(&wire::le32(seed)).await?;
336    if let Some(refusal) = &req.refusal {
337        // Said at once: an uploading client sends its own file list next, not a filter list,
338        // and an argument this daemon cannot honour must stop it before that list is read.
339        decision(ctx, id, "request", "refused_options");
340        out.message(wire::MSG_ERROR_XFER, &format!("ERROR: {refusal}"))
341            .await?;
342        close_and_drain(&mut input, &mut out).await;
343        return Ok(());
344    }
345    if !req.sender {
346        return upload(ctx, id, &mut input, &mut out, &module, &req, seed, &client).await;
347    }
348    // The filter list (raw at 29), read whole before anything else is sent.
349    let mut filter_bytes = 0usize;
350    loop {
351        let n = input.int().await?;
352        if n == 0 {
353            break;
354        }
355        ensure!(n > 0, "a negative filter-rule length");
356        filter_bytes += n as usize;
357        ensure!(
358            filter_bytes <= wire::MAX_FILTER_BYTES,
359            "more than {} bytes of filter rules",
360            wire::MAX_FILTER_BYTES
361        );
362        input.exact(n as usize).await?;
363    }
364    if req.paths.is_empty() || req.paths.len() > MAX_PATHS {
365        out.message(
366            wire::MSG_ERROR_XFER,
367            "ERROR: no path, or too many paths, in the request",
368        )
369        .await?;
370        return Ok(());
371    }
372
373    // Ask the model what each requested path holds.
374    let mut model_entries: Vec<Entry> = Vec::new();
375    let now = now_secs();
376    for path in &req.paths {
377        let event = Event::new(
378            &actions::REQUEST_EVENT,
379            json!({"module": module, "path": path, "recursive": req.recursive, "list_only": req.list_only, "client": client}),
380        );
381        match ask(ctx, id, event).await {
382            Ok(Some((name, data))) if name == actions::ENTRIES => {
383                for e in data["entries"].as_array().into_iter().flatten() {
384                    model_entries.push(wire::entry_from_json(e, now)?);
385                }
386            }
387            Ok(Some((name, data))) if name == actions::REFUSE => {
388                decision(ctx, id, "request", "model_refused");
389                let msg = data["message"].as_str().unwrap_or("refused");
390                out.message(wire::MSG_ERROR_XFER, &format!("@ERROR: {msg}"))
391                    .await?;
392                return Ok(());
393            }
394            Ok(_) => {
395                // Nothing said about this path: it does not exist.
396            }
397            Err(e) => {
398                decision(ctx, id, "request", "fail_closed_llm_error");
399                let text = crate::utils::wire_failure::wire_failure_text(&e);
400                out.message(wire::MSG_ERROR_XFER, &format!("ERROR: {text}"))
401                    .await?;
402                return Ok(());
403            }
404        }
405    }
406    complete_dirs(&mut model_entries, now);
407    let (list, notes, missing) = select(&model_entries, &req, now);
408    for (code, text) in &notes {
409        out.message(*code, text).await?;
410    }
411    let mut bytes = Vec::new();
412    let mut writer = wire::FlistWriter::default();
413    for (e, top) in &list {
414        writer.entry(e, *top, &req, &mut bytes);
415    }
416    wire::FlistWriter::finish(&req, i32::from(missing), &mut bytes);
417    out.data(&bytes).await?;
418    decision(
419        ctx,
420        id,
421        "request",
422        &format!("model_answered entries={}", list.len()),
423    );
424    if list.is_empty() {
425        // The receiver sends nothing more for an empty list; rsync's sender exits here.
426        let _ = out.wr.shutdown().await;
427        return Ok(());
428    }
429
430    // Requests, three NDX_DONE phases, stats, goodbye.
431    let mut phase = 0;
432    let mut sent_files = 0;
433    loop {
434        let ndx = input.int().await?;
435        if ndx == wire::NDX_DONE {
436            phase += 1;
437            if phase > 2 {
438                break;
439            }
440            out.data(&wire::le32(wire::NDX_DONE)).await?;
441            continue;
442        }
443        let iflags = input.short().await?;
444        let mut echo = Vec::new();
445        echo.extend_from_slice(&wire::le32(ndx));
446        echo.extend_from_slice(&iflags.to_le_bytes());
447        if iflags & wire::ITEM_BASIS_TYPE_FOLLOWS != 0 {
448            echo.extend(input.exact(1).await?);
449        }
450        if iflags & wire::ITEM_XNAME_FOLLOWS != 0 {
451            let mut len = input.exact(1).await?;
452            let mut n = len[0] as usize;
453            if n & 0x80 != 0 {
454                let low = input.exact(1).await?[0];
455                n = ((n & 0x7f) << 8) | low as usize;
456                len.push(low);
457            }
458            echo.extend(len);
459            echo.extend(input.exact(n).await?);
460        }
461        if ndx as usize == list.len() && iflags == wire::ITEM_IS_NEW {
462            continue; // a protocol-29 keep-alive
463        }
464        ensure!(
465            ndx >= 0 && (ndx as usize) < list.len(),
466            "request for index {ndx} of {}",
467            list.len()
468        );
469        let entry = &list[ndx as usize].0;
470        if iflags & wire::ITEM_TRANSFER == 0 || req.dry_run {
471            out.data(&echo).await?;
472            continue;
473        }
474        ensure!(
475            entry.kind == Kind::File,
476            "request to transfer non-regular file {:?}",
477            entry.path
478        );
479        let count = input.int().await?;
480        let blength = input.int().await?;
481        let s2length = input.int().await?;
482        let remainder = input.int().await?;
483        ensure!(
484            (0..=wire::MAX_SUM_COUNT).contains(&count)
485                && (0..=16).contains(&s2length)
486                && blength >= 0
487                && remainder >= 0,
488            "a malformed checksum header"
489        );
490        // Block checksums are read and not used: every file is sent whole.
491        input.skip(count as u64 * (4 + s2length as u64)).await?;
492        let mut reply = echo;
493        for v in [count, blength, s2length, remainder] {
494            reply.extend_from_slice(&wire::le32(v));
495        }
496        for chunk in entry.data.chunks(wire::CHUNK) {
497            reply.extend_from_slice(&wire::le32(chunk.len() as i32));
498            reply.extend_from_slice(chunk);
499        }
500        reply.extend_from_slice(&wire::le32(0));
501        reply.extend_from_slice(&wire::file_sum(seed, &entry.data));
502        out.data(&reply).await?;
503        sent_files += 1;
504    }
505    let total_size: u64 = list
506        .iter()
507        .filter(|(e, _)| e.kind != Kind::Dir)
508        .map(|(e, _)| e.size)
509        .sum();
510    let mut tail = wire::le32(wire::NDX_DONE).to_vec();
511    let written = out.total + 4 + 4 + 20;
512    for v in [input.total, written, total_size, 1, 0] {
513        tail.extend(wire::longint(v));
514    }
515    out.data(&tail).await?;
516    let goodbye = input.int().await?;
517    ensure!(
518        goodbye == wire::NDX_DONE,
519        "expected the final goodbye, got {goodbye}"
520    );
521    ctx.state
522        .update_connection_stats(
523            ctx.server_id,
524            id,
525            Some(input.total),
526            Some(out.total),
527            None,
528            None,
529        )
530        .await;
531    Log::new(Some(&ctx.status_tx)).info(format!(
532        "rsync connection {id}: module {module}, {} entries, {sent_files} files sent",
533        list.len()
534    ));
535    let _ = out.wr.shutdown().await;
536    Ok(())
537}
538
539/// The module-relative path of an entry an uploading client listed under `dest`.
540fn upload_path(dest: &str, entry: &str) -> String {
541    let dest = dest.trim_end_matches('/');
542    match (dest.is_empty(), entry) {
543        (_, ".") => dest.to_string(),
544        (true, e) => e.to_string(),
545        (false, e) => format!("{dest}/{e}"),
546    }
547}
548
549/// The receiving side of a push (protocol 29: the client's bytes raw, ours multiplexed).
550///
551/// The client's file list comes first; the model decides which files to take; each taken file
552/// is requested whole (an empty checksum header, as for a new file), read as literal tokens and
553/// checked against MD4(seed ‖ data); then the end-of-run exchange the stock receiver makes:
554/// NDX_DONE, the client's echo, two more each way, and a final one. When the model accepted,
555/// the files are handed to it in `rsync_upload_complete`; after a refusal, silence or a backend
556/// failure the exchange still ends cleanly but no completion event is raised. Nothing is
557/// written anywhere.
558#[allow(clippy::too_many_arguments)]
559async fn upload(
560    ctx: &SpawnContext,
561    id: ConnectionId,
562    input: &mut In,
563    out: &mut Out,
564    module: &str,
565    req: &Request,
566    seed: i32,
567    client: &str,
568) -> Result<()> {
569    let dest = req.paths.first().cloned().unwrap_or_default();
570    let mut reader = wire::MuxReader::raw(&mut input.rd);
571    let listed = tokio::time::timeout(
572        input.idle,
573        reader.file_list_bounded(req, wire::FlistLimits::UPLOAD),
574    )
575    .await;
576    input.total += reader.consumed;
577    let (mut entries, _io_error) = match listed {
578        Ok(Ok(list)) => list,
579        Ok(Err(e)) => {
580            // Over a bound, or malformed: the model never sees the list.
581            decision(ctx, id, "upload", "refused_file_list");
582            out.message(
583                wire::MSG_ERROR_XFER,
584                &format!("ERROR: file list refused: {e}"),
585            )
586            .await?;
587            close_and_drain(input, out).await;
588            return Err(e.context("the client's file list"));
589        }
590        Err(_) => bail!("the client went quiet sending its file list"),
591    };
592    // Indexes are positions in rsync's order, as the client numbers them too.
593    wire::sort(&mut entries);
594    entries.dedup_by(|a, b| a.path == b.path);
595
596    let files: Vec<Value> = entries
597        .iter()
598        .map(|e| {
599            let mut v = json!({
600                "path": upload_path(&dest, &e.path),
601                "type": e.kind.name(),
602                "size": e.size,
603                "mode": format!("{:o}", e.mode & 0o7777),
604            });
605            if e.kind == Kind::Symlink {
606                v["target"] = json!(String::from_utf8_lossy(&e.data));
607            }
608            v
609        })
610        .collect();
611    let regular: Vec<usize> = (0..entries.len())
612        .filter(|&i| entries[i].kind == Kind::File)
613        .collect();
614    let event = Event::new(
615        &actions::UPLOAD_REQUEST_EVENT,
616        json!({
617            "module": module,
618            "path": dest,
619            "files": files,
620            "file_count": regular.len(),
621            // Declared sizes are the client's to choose: added without overflowing.
622            "total_bytes": regular.iter().fold(0u64, |t, &i| t.saturating_add(entries[i].size)),
623            "client": client,
624        }),
625    );
626    // Which files to take, and why each other one is not taken.
627    let mut take: Vec<usize> = Vec::new();
628    let mut refused: Vec<(String, String)> = Vec::new();
629    let mut accepted = false;
630    match ask(ctx, id, event).await {
631        Ok(Some((name, data))) if name == actions::ACCEPT_UPLOAD => {
632            accepted = true;
633            let wanted: Option<std::collections::HashSet<String>> =
634                data.get("paths").and_then(|p| p.as_array()).map(|l| {
635                    l.iter()
636                        .filter_map(|p| p.as_str().map(String::from))
637                        .collect()
638                });
639            let mut budget = 0u64;
640            for &i in &regular {
641                let path = upload_path(&dest, &entries[i].path);
642                let reason = if wanted.as_ref().is_some_and(|w| !w.contains(&path)) {
643                    Some("refused by the daemon")
644                } else if entries[i].size > MAX_UPLOAD_FILE_BYTES {
645                    Some("larger than this daemon accepts")
646                } else if take.len() >= MAX_UPLOAD_FILES
647                    || budget + entries[i].size > MAX_UPLOAD_BYTES
648                {
649                    Some("over this daemon's upload limit")
650                } else {
651                    None
652                };
653                match reason {
654                    Some(r) => refused.push((path, r.to_string())),
655                    None => {
656                        budget += entries[i].size;
657                        take.push(i);
658                    }
659                }
660            }
661            decision(
662                ctx,
663                id,
664                "upload",
665                &format!(
666                    "model_accepted taken={} refused={}",
667                    take.len(),
668                    refused.len()
669                ),
670            );
671        }
672        Ok(Some((name, data))) if name == actions::REFUSE => {
673            decision(ctx, id, "upload", "model_refused");
674            let msg = data["message"].as_str().unwrap_or("refused");
675            out.message(wire::MSG_ERROR_XFER, &format!("@ERROR: {msg}"))
676                .await?;
677        }
678        Ok(_) => {
679            // No decision is not consent.
680            decision(ctx, id, "upload", "fail_closed_no_decision");
681            out.message(wire::MSG_ERROR_XFER, "ERROR: upload refused by the daemon")
682                .await?;
683        }
684        Err(e) => {
685            decision(ctx, id, "upload", "fail_closed_llm_error");
686            let text = crate::utils::wire_failure::wire_failure_text(&e);
687            out.message(wire::MSG_ERROR_XFER, &format!("ERROR: {text}"))
688                .await?;
689        }
690    }
691    for (path, reason) in &refused {
692        out.message(
693            wire::MSG_ERROR_XFER,
694            &format!("rsync: [receiver] {path}: {reason}"),
695        )
696        .await?;
697    }
698
699    // Request each taken file and read it back, one at a time.
700    let mut received: Vec<Value> = entries
701        .iter()
702        .filter(|e| e.kind != Kind::File)
703        .map(|e| json!({"path": upload_path(&dest, &e.path), "type": e.kind.name()}))
704        .collect();
705    let mut failed: Vec<(String, String)> = Vec::new();
706    // File data received so far in this upload, counted as it arrives.
707    let mut received_bytes = 0u64;
708    for &i in &take {
709        let entry = &entries[i];
710        let path = upload_path(&dest, &entry.path);
711        let mut request = wire::le32(i as i32).to_vec();
712        request.extend_from_slice(&(wire::ITEM_TRANSFER | wire::ITEM_IS_NEW).to_le_bytes());
713        for _ in 0..4 {
714            request.extend_from_slice(&wire::le32(0));
715        }
716        out.data(&request).await?;
717        let ndx = input.int().await?;
718        ensure!(ndx == i as i32, "asked for file {i}, the client sent {ndx}");
719        let iflags = input.short().await?;
720        ensure!(
721            iflags & wire::ITEM_TRANSFER != 0
722                && iflags & (wire::ITEM_BASIS_TYPE_FOLLOWS | wire::ITEM_XNAME_FOLLOWS) == 0,
723            "the client answered a whole-file request with item flags {iflags:#06x}"
724        );
725        let mut head = [0i32; 4];
726        for v in head.iter_mut() {
727            *v = input.int().await?;
728        }
729        ensure!(
730            head == [0, 0, 0, 0],
731            "the client echoed checksum header {head:?} for a request without one"
732        );
733        // Data is kept up to the file's declared size (which was checked against the bounds
734        // before the file was requested); anything past it is read, dropped, and refuses the
735        // file. Every byte counts toward the upload's MAX_UPLOAD_BYTES as it arrives.
736        let mut data = Vec::new();
737        let mut sent = 0u64;
738        let mut over_declared = false;
739        loop {
740            let token = input.int().await?;
741            if token == 0 {
742                break;
743            }
744            ensure!(
745                token > 0,
746                "a block reference in a file sent without a basis"
747            );
748            let token = token as u64;
749            sent += token;
750            received_bytes += token;
751            if received_bytes > MAX_UPLOAD_BYTES {
752                decision(ctx, id, "upload", "refused_over_upload_limit");
753                out.message(
754                    wire::MSG_ERROR_XFER,
755                    &format!("rsync: [receiver] {path}: over this daemon's upload limit"),
756                )
757                .await?;
758                close_and_drain(input, out).await;
759                bail!("the upload sent more than {MAX_UPLOAD_BYTES} bytes of file data");
760            }
761            ensure!(
762                sent <= MAX_UPLOAD_FILE_BYTES,
763                "{path} grew past {MAX_UPLOAD_FILE_BYTES} bytes while it was sent"
764            );
765            if over_declared || data.len() as u64 + token > entry.size {
766                over_declared = true;
767                input.skip(token).await?;
768            } else {
769                data.extend(input.exact(token as usize).await?);
770            }
771        }
772        let sum = input.exact(16).await?;
773        if over_declared {
774            out.message(
775                wire::MSG_ERROR_XFER,
776                &format!(
777                    "rsync: [receiver] {path}: more data than its listed size, file discarded"
778                ),
779            )
780            .await?;
781            failed.push((path, "more data than its listed size".into()));
782            continue;
783        }
784        if sum != wire::file_sum(seed, &data) {
785            out.message(
786                wire::MSG_ERROR_XFER,
787                &format!("rsync: [receiver] {path}: checksum mismatch, file discarded"),
788            )
789            .await?;
790            failed.push((path, "checksum mismatch".into()));
791            continue;
792        }
793        let (content, encoding) = wire::content_json(&data);
794        received.push(json!({"path": path, "type": "file", "size": data.len(), "content": content, "encoding": encoding}));
795    }
796
797    // The end of the run, as the stock receiver makes it.
798    out.data(&wire::le32(wire::NDX_DONE)).await?;
799    ensure!(
800        input.int().await? == wire::NDX_DONE,
801        "the client did not end its phase"
802    );
803    let mut two = wire::le32(wire::NDX_DONE).to_vec();
804    two.extend_from_slice(&wire::le32(wire::NDX_DONE));
805    out.data(&two).await?;
806    for _ in 0..2 {
807        ensure!(
808            input.int().await? == wire::NDX_DONE,
809            "the client did not end the run"
810        );
811    }
812    out.data(&wire::le32(wire::NDX_DONE)).await?;
813    let _ = out.wr.shutdown().await;
814    ctx.state
815        .update_connection_stats(
816            ctx.server_id,
817            id,
818            Some(input.total),
819            Some(out.total),
820            None,
821            None,
822        )
823        .await;
824
825    if !accepted {
826        // A refused upload delivered nothing, so there is nothing to complete.
827        return Ok(());
828    }
829    let taken = received.iter().filter(|f| f["type"] == "file").count();
830    Log::new(Some(&ctx.status_tx)).info(format!(
831        "rsync connection {id}: upload to {module}/{dest}, {taken} file(s) taken, {} refused",
832        refused.len() + failed.len()
833    ));
834    refused.extend(failed);
835    let event = Event::new(
836        &actions::UPLOAD_COMPLETE_EVENT,
837        json!({
838            "module": module,
839            "path": dest,
840            "files": received,
841            "refused": refused.iter().map(|(p, r)| json!({"path": p, "reason": r})).collect::<Vec<_>>(),
842            "client": client,
843        }),
844    );
845    if let Err(e) = ask(ctx, id, event).await {
846        decision(ctx, id, "upload_complete", "fail_closed_llm_error");
847        Log::new(Some(&ctx.status_tx)).warn(format!("rsync upload_complete handler failed: {e:#}"));
848    }
849    Ok(())
850}
851
852async fn list_modules(
853    ctx: &SpawnContext,
854    id: ConnectionId,
855    out: &mut Out,
856    client: &str,
857) -> Result<()> {
858    let event = Event::new(&actions::LIST_MODULES_EVENT, json!({"client": client}));
859    let mut listing = String::new();
860    match ask(ctx, id, event).await {
861        Ok(Some((name, data))) if name == actions::MODULES => {
862            for m in data["modules"].as_array().into_iter().flatten() {
863                let name = m["name"].as_str().unwrap_or_default();
864                let comment =
865                    crate::utils::sanitize::line_field(m["comment"].as_str().unwrap_or_default());
866                listing.push_str(&format!("{name:<15}\t{comment}\n"));
867            }
868            decision(ctx, id, "list_modules", "model_answered");
869        }
870        Ok(_) => decision(ctx, id, "list_modules", "model_silent"),
871        Err(e) => {
872            decision(ctx, id, "list_modules", "fail_closed_llm_error");
873            let text = crate::utils::wire_failure::wire_failure_text(&e);
874            out.raw(format!("@ERROR: {text}\n").as_bytes()).await?;
875            return Ok(());
876        }
877    }
878    listing.push_str("@RSYNCD: EXIT\n");
879    out.raw(listing.as_bytes()).await
880}
881
882/// Add the directories the model implied but did not list (a/b/c.txt implies a and a/b), and
883/// keep one entry per path (the first).
884/// Drop duplicate paths and add the directories the remaining paths imply.
885pub fn complete_dirs(entries: &mut Vec<Entry>, now: u32) {
886    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
887    entries.retain(|e| seen.insert(e.path.clone()));
888    let mut add = Vec::new();
889    for e in entries.iter() {
890        let mut p = e.path.as_str();
891        while let Some(i) = p.rfind('/') {
892            p = &p[..i];
893            if seen.insert(p.to_string()) {
894                add.push(
895                    wire::entry_from_json(&json!({"path": p, "type": "dir"}), now)
896                        .expect("a valid dir"),
897                );
898            }
899        }
900    }
901    entries.extend(add);
902}
903
904/// The entries to send (each with its top-dir flag), messages for the client, and whether a
905/// requested path was missing.
906type Selection = (Vec<(Entry, bool)>, Vec<(u8, String)>, bool);
907
908/// What rsync's sender would list for each requested path (flist.c send_file_list): a
909/// trailing-slash directory becomes "." plus its contents; a named file its basename; a named
910/// directory itself (and, with -r, its subtree). Returns the list in rsync's order, messages
911/// for the client, and whether anything requested was missing.
912fn select(all: &[Entry], req: &Request, now: u32) -> Selection {
913    let mut out: Vec<(Entry, bool)> = Vec::new();
914    let mut notes = Vec::new();
915    let mut missing = false;
916    let find = |p: &str| all.iter().find(|e| e.path == p);
917    let under = |dir: &str, deep: bool| -> Vec<Entry> {
918        all.iter()
919            .filter_map(|e| {
920                let rel = if dir.is_empty() {
921                    e.path.as_str()
922                } else {
923                    e.path.strip_prefix(&format!("{dir}/"))?
924                };
925                (deep || !rel.contains('/')).then(|| Entry {
926                    path: rel.to_string(),
927                    ..e.clone()
928                })
929            })
930            .collect()
931    };
932    for path in &req.paths {
933        let contents = path.is_empty() || path.ends_with('/');
934        let base = path.trim_end_matches('/');
935        if contents {
936            let dir = if base.is_empty() {
937                wire::entry_from_json(&json!({"path": "root", "type": "dir"}), now)
938                    .expect("a valid dir")
939            } else {
940                match find(base) {
941                    Some(e) if e.kind == Kind::Dir => e.clone(),
942                    _ => {
943                        missing = true;
944                        notes.push((wire::MSG_ERROR_XFER, format!("rsync: [sender] change_dir \"{base}\" failed: No such file or directory (2)")));
945                        continue;
946                    }
947                }
948            };
949            if !(req.recursive || req.dirs) {
950                notes.push((wire::MSG_INFO, "skipping directory .".into()));
951                continue;
952            }
953            out.push((
954                Entry {
955                    path: ".".into(),
956                    ..dir
957                },
958                true,
959            ));
960            out.extend(under(base, req.recursive).into_iter().map(|e| (e, false)));
961            continue;
962        }
963        let Some(e) = find(base) else {
964            missing = true;
965            notes.push((
966                wire::MSG_ERROR_XFER,
967                format!(
968                    "rsync: [sender] link_stat \"{base}\" failed: No such file or directory (2)"
969                ),
970            ));
971            continue;
972        };
973        let name = base.rsplit('/').next().unwrap_or(base).to_string();
974        match e.kind {
975            Kind::Dir if req.recursive => {
976                out.push((
977                    Entry {
978                        path: name.clone(),
979                        ..e.clone()
980                    },
981                    true,
982                ));
983                out.extend(under(base, true).into_iter().map(|c| {
984                    (
985                        Entry {
986                            path: format!("{name}/{}", c.path),
987                            ..c
988                        },
989                        false,
990                    )
991                }));
992            }
993            Kind::Dir if req.dirs => out.push((
994                Entry {
995                    path: name,
996                    ..e.clone()
997                },
998                false,
999            )),
1000            Kind::Dir => notes.push((wire::MSG_INFO, format!("skipping directory {name}"))),
1001            _ => out.push((
1002                Entry {
1003                    path: name,
1004                    ..e.clone()
1005                },
1006                false,
1007            )),
1008        }
1009    }
1010    out.sort_by_cached_key(|(e, _)| wire::sort_key(&e.path, e.is_dir()));
1011    out.dedup_by(|a, b| a.0.path == b.0.path);
1012    (out, notes, missing)
1013}