1pub 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
23pub const IDLE_TIMEOUT: Duration = Duration::from_secs(120);
25pub const MAX_IDLE_TIMEOUT_SECS: u64 = 3600;
26pub const MAX_PATHS: usize = 16;
28const WRITE_TIMEOUT: Duration = Duration::from_secs(60);
29pub const MAX_UPLOAD_FILE_BYTES: u64 = 1024 * 1024;
31pub const MAX_UPLOAD_BYTES: u64 = 8 * 1024 * 1024;
34pub 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 };
42pub const MAX_MESSAGE_BYTES: usize = 4000;
44pub 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
136struct 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 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 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
214pub 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
226async 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 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 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 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 }
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 ¬es {
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 let _ = out.wr.shutdown().await;
427 return Ok(());
428 }
429
430 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; }
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 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
539fn 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#[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 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 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 "total_bytes": regular.iter().fold(0u64, |t, &i| t.saturating_add(entries[i].size)),
623 "client": client,
624 }),
625 );
626 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 ®ular {
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 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 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 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 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 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 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
882pub 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
904type Selection = (Vec<(Entry, bool)>, Vec<(u8, String)>, bool);
907
908fn 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}