1use anyhow::{bail, ensure, Context, Result};
11use md4::{Digest, Md4};
12use serde_json::{json, Value};
13use tokio::io::{AsyncRead, AsyncReadExt};
14
15pub const PROTOCOL: i32 = 29;
16pub const GREETING: &str = "@RSYNCD: 29.0\n";
17pub const NDX_DONE: i32 = -1;
18const MPLEX_BASE: u8 = 7;
19pub const MSG_DATA: u8 = 0;
20pub const MSG_ERROR_XFER: u8 = 1;
21pub const MSG_INFO: u8 = 2;
22pub const MSG_ERROR: u8 = 3;
23pub const MSG_WARNING: u8 = 4;
24pub const MSG_IO_ERROR: u8 = 22;
25pub const MSG_NOOP: u8 = 42;
26pub const MSG_ERROR_EXIT: u8 = 86;
27
28pub const ITEM_TRANSFER: u16 = 0x8000;
29pub const ITEM_IS_NEW: u16 = 0x2000;
30pub const ITEM_XNAME_FOLLOWS: u16 = 0x1000;
31pub const ITEM_BASIS_TYPE_FOLLOWS: u16 = 0x0800;
32
33const XMIT_TOP_DIR: u16 = 0x01;
34const XMIT_SAME_MODE: u16 = 0x02;
35const XMIT_EXTENDED_FLAGS: u16 = 0x04;
36const XMIT_SAME_UID: u16 = 0x08;
37const XMIT_SAME_GID: u16 = 0x10;
38const XMIT_SAME_NAME: u16 = 0x20;
39const XMIT_LONG_NAME: u16 = 0x40;
40const XMIT_SAME_TIME: u16 = 0x80;
41
42pub const CHUNK: usize = 32 * 1024;
44pub const MAX_LINE: usize = 4096;
46pub const MAX_ARGS: usize = 64;
47pub const MAX_NAME: usize = 4096;
49pub const MAX_ENTRIES: usize = 100_000;
51pub const MAX_UPLOAD_LIST_ENTRIES: usize = 2 * super::MAX_UPLOAD_FILES;
54pub const MAX_UPLOAD_LIST_NAME_BYTES: usize = 1024 * 1024;
57pub const MAX_FILTER_BYTES: usize = 64 * 1024;
59pub const MAX_SUM_COUNT: i32 = 1 << 20;
61
62pub const S_IFMT: u32 = 0o170000;
63pub const S_IFREG: u32 = 0o100000;
64pub const S_IFDIR: u32 = 0o040000;
65pub const S_IFLNK: u32 = 0o120000;
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum Kind {
69 File,
70 Dir,
71 Symlink,
72}
73
74impl Kind {
75 pub fn name(self) -> &'static str {
76 match self {
77 Kind::File => "file",
78 Kind::Dir => "dir",
79 Kind::Symlink => "symlink",
80 }
81 }
82 pub fn of_mode(mode: u32) -> Option<Self> {
83 match mode & S_IFMT {
84 S_IFREG => Some(Kind::File),
85 S_IFDIR => Some(Kind::Dir),
86 S_IFLNK => Some(Kind::Symlink),
87 _ => None,
88 }
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct Entry {
95 pub path: String,
96 pub kind: Kind,
97 pub data: Vec<u8>,
99 pub mode: u32,
100 pub mtime: u32,
101 pub uid: u32,
102 pub gid: u32,
103 pub size: u64,
105}
106
107impl Entry {
108 pub fn is_dir(&self) -> bool {
109 self.kind == Kind::Dir
110 }
111}
112
113pub fn le32(x: i32) -> [u8; 4] {
114 x.to_le_bytes()
115}
116
117pub fn longint(x: u64) -> Vec<u8> {
118 if x <= 0x7fff_ffff {
119 (x as i32).to_le_bytes().to_vec()
120 } else {
121 let mut v = vec![0xff; 4];
122 v.extend_from_slice(&(x as i64).to_le_bytes());
123 v
124 }
125}
126
127pub fn md4(parts: &[&[u8]]) -> [u8; 16] {
129 let mut h = Md4::new();
130 for p in parts {
131 h.update(p);
132 }
133 h.finalize().into()
134}
135
136pub fn file_sum(seed: i32, data: &[u8]) -> [u8; 16] {
138 md4(&[&seed.to_le_bytes(), data])
139}
140
141pub fn list_sum(data: &[u8]) -> [u8; 16] {
143 md4(&[data])
144}
145
146pub fn auth_response(password: &str, challenge: &str) -> String {
150 use base64::Engine;
151 let digest = md4(&[&[0u8; 4], password.as_bytes(), challenge.as_bytes()]);
152 base64::engine::general_purpose::STANDARD_NO_PAD.encode(digest)
153}
154
155pub fn frame(code: u8, payload: &[u8]) -> Vec<u8> {
157 let mut out = Vec::with_capacity(4 + payload.len());
158 let header = ((u32::from(MPLEX_BASE + code)) << 24) | payload.len() as u32;
159 out.extend_from_slice(&header.to_le_bytes());
160 out.extend_from_slice(payload);
161 out
162}
163
164pub fn data_frames(bytes: &[u8]) -> Vec<u8> {
166 let mut out = Vec::with_capacity(bytes.len() + 4 * (bytes.len() / 0xffff + 1));
167 for chunk in bytes.chunks(0xffff) {
168 out.extend_from_slice(&frame(MSG_DATA, chunk));
169 }
170 out
171}
172
173pub fn sort_key(path: &str, is_dir: bool) -> Vec<(u8, Vec<u8>)> {
177 if path == "." {
178 return vec![];
179 }
180 let comps: Vec<&[u8]> = path.as_bytes().split(|&b| b == b'/').collect();
181 let mut key = Vec::with_capacity(comps.len());
182 for c in &comps[..comps.len() - 1] {
183 key.push((1, [*c, b"/"].concat()));
184 }
185 let last = comps[comps.len() - 1];
186 key.push(if is_dir {
187 (1, [last, b"/"].concat())
188 } else {
189 (0, last.to_vec())
190 });
191 key
192}
193
194pub fn sort(entries: &mut [Entry]) {
195 entries.sort_by_cached_key(|e| sort_key(&e.path, e.is_dir()));
196}
197
198#[derive(Debug, Default, Clone)]
200pub struct Request {
201 pub sender: bool,
202 pub recursive: bool,
203 pub dirs: bool,
204 pub links: bool,
205 pub owner: bool,
206 pub group: bool,
207 pub dry_run: bool,
208 pub checksum: bool,
209 pub numeric_ids: bool,
210 pub list_only: bool,
211 pub devices: bool,
213 pub hard_links: bool,
215 pub checksum_seed: Option<i32>,
216 pub paths: Vec<String>,
218 pub refusal: Option<String>,
220}
221
222pub fn parse_args(args: &[String], module: &str) -> Request {
224 let mut r = Request::default();
225 let mut paths = false;
226 for a in args {
227 if paths {
228 let p = if a == module {
229 ""
230 } else if let Some(rest) = a.strip_prefix(&format!("{module}/")) {
231 rest
232 } else {
233 r.refusal
234 .get_or_insert(format!("path {a:?} is outside module {module}"));
235 continue;
236 };
237 r.paths.push(p.to_string());
238 continue;
239 }
240 match a.as_str() {
241 "." => paths = true,
242 "--server" => {}
243 "--sender" => r.sender = true,
244 "--numeric-ids" => r.numeric_ids = true,
245 "--list-only" => r.list_only = true,
246 "--devices" | "--specials" => r.devices = true,
247 s if s.starts_with("--delete") || s == "--del" => {
251 r.refusal
252 .get_or_insert(format!("{s} is not supported by this daemon"));
253 }
254 s if s.starts_with("--checksum-seed=") => {
255 r.checksum_seed = s["--checksum-seed=".len()..].parse().ok();
256 }
257 s if s.starts_with("--files-from") || s == "--from0" || s == "--secluded-args" => {
258 r.refusal
259 .get_or_insert(format!("{s} is not supported by this daemon"));
260 }
261 s if s.starts_with("--") => {}
262 s if s.starts_with('-') => {
263 for c in s[1..].chars() {
264 match c {
265 'e' => break, 'r' => r.recursive = true,
267 'd' => r.dirs = true,
268 'l' => r.links = true,
269 'o' => r.owner = true,
270 'g' => r.group = true,
271 'n' => r.dry_run = true,
272 'c' => r.checksum = true,
273 'D' => r.devices = true,
274 'H' => r.hard_links = true,
275 'z' | 'U' | 'N' | 'R' | 's' => {
276 r.refusal
277 .get_or_insert(format!("-{c} is not supported by this daemon"));
278 }
279 _ => {}
280 }
281 }
282 }
283 _ => {}
284 }
285 }
286 if !r.sender && r.paths.len() > 1 {
287 r.refusal
288 .get_or_insert("an upload names one destination".into());
289 }
290 if !r.sender && r.hard_links {
291 r.refusal
292 .get_or_insert("-H is not supported for uploads by this daemon".into());
293 }
294 r
295}
296
297#[derive(Default)]
299pub struct FlistWriter {
300 last_name: Vec<u8>,
301 mode: Option<u32>,
302 mtime: Option<u32>,
303 uid: Option<u32>,
304 gid: Option<u32>,
305}
306
307impl FlistWriter {
308 pub fn entry(&mut self, e: &Entry, top: bool, r: &Request, out: &mut Vec<u8>) {
310 let name = e.path.as_bytes();
311 let first = self.mode.is_none();
312 let mut x: u16 = 0;
313 if e.is_dir() && top {
314 x |= XMIT_TOP_DIR;
315 }
316 if self.mode == Some(e.mode) {
317 x |= XMIT_SAME_MODE;
318 }
319 if !r.owner || (!first && self.uid == Some(e.uid)) {
320 x |= XMIT_SAME_UID;
321 }
322 if !r.group || (!first && self.gid == Some(e.gid)) {
323 x |= XMIT_SAME_GID;
324 }
325 if self.mtime == Some(e.mtime) {
326 x |= XMIT_SAME_TIME;
327 }
328 let l1 = self
329 .last_name
330 .iter()
331 .zip(name)
332 .take(255)
333 .take_while(|(a, b)| a == b)
334 .count();
335 let l2 = name.len() - l1;
336 if l1 > 0 {
337 x |= XMIT_SAME_NAME;
338 }
339 if l2 > 255 {
340 x |= XMIT_LONG_NAME;
341 }
342 if x == 0 && !e.is_dir() {
343 x |= XMIT_TOP_DIR; }
345 if x & 0xff00 != 0 || x == 0 {
346 x |= XMIT_EXTENDED_FLAGS;
347 out.extend_from_slice(&x.to_le_bytes());
348 } else {
349 out.push(x as u8);
350 }
351 if x & XMIT_SAME_NAME != 0 {
352 out.push(l1 as u8);
353 }
354 if x & XMIT_LONG_NAME != 0 {
355 out.extend_from_slice(&le32(l2 as i32));
356 } else {
357 out.push(l2 as u8);
358 }
359 out.extend_from_slice(&name[l1..]);
360 out.extend_from_slice(&longint(e.size));
361 if x & XMIT_SAME_TIME == 0 {
362 out.extend_from_slice(&e.mtime.to_le_bytes());
363 }
364 if x & XMIT_SAME_MODE == 0 {
365 out.extend_from_slice(&e.mode.to_le_bytes());
366 }
367 if r.owner && x & XMIT_SAME_UID == 0 {
368 out.extend_from_slice(&e.uid.to_le_bytes());
369 }
370 if r.group && x & XMIT_SAME_GID == 0 {
371 out.extend_from_slice(&e.gid.to_le_bytes());
372 }
373 if r.links && e.kind == Kind::Symlink {
374 out.extend_from_slice(&le32(e.data.len() as i32));
375 out.extend_from_slice(&e.data);
376 }
377 if r.checksum && e.kind == Kind::File {
378 out.extend_from_slice(&list_sum(&e.data));
379 }
380 self.last_name = name.to_vec();
381 self.mode = Some(e.mode);
382 self.mtime = Some(e.mtime);
383 self.uid = Some(e.uid);
384 self.gid = Some(e.gid);
385 }
386
387 pub fn finish(r: &Request, io_error: i32, out: &mut Vec<u8>) {
389 out.push(0);
390 if r.owner && !r.numeric_ids {
391 out.extend_from_slice(&le32(0));
392 }
393 if r.group && !r.numeric_ids {
394 out.extend_from_slice(&le32(0));
395 }
396 out.extend_from_slice(&le32(io_error));
397 }
398}
399
400#[derive(Debug, Clone, Copy, PartialEq, Eq)]
403pub struct FlistLimits {
404 pub max_entries: usize,
405 pub max_name_bytes: usize,
406}
407
408impl FlistLimits {
409 pub const DOWNLOAD: Self = Self {
412 max_entries: MAX_ENTRIES,
413 max_name_bytes: usize::MAX,
414 };
415 pub const UPLOAD: Self = Self {
417 max_entries: MAX_UPLOAD_LIST_ENTRIES,
418 max_name_bytes: MAX_UPLOAD_LIST_NAME_BYTES,
419 };
420}
421
422pub struct MuxReader<R> {
424 inner: R,
425 left: usize,
426 mux: bool,
428 pub consumed: u64,
430 pub messages: Vec<(u8, String)>,
431 pub io_error: i32,
432}
433
434impl<R: AsyncRead + Unpin> MuxReader<R> {
435 pub fn new(inner: R) -> Self {
436 Self {
437 inner,
438 left: 0,
439 mux: true,
440 consumed: 0,
441 messages: vec![],
442 io_error: 0,
443 }
444 }
445 pub fn raw(inner: R) -> Self {
448 Self {
449 mux: false,
450 ..Self::new(inner)
451 }
452 }
453 pub async fn read(&mut self, n: usize) -> Result<Vec<u8>> {
454 let mut out = vec![0u8; n];
455 if !self.mux {
456 self.inner
457 .read_exact(&mut out)
458 .await
459 .context("the peer closed the connection")?;
460 self.consumed += n as u64;
461 return Ok(out);
462 }
463 let mut have = 0;
464 while have < n {
465 while self.left == 0 {
466 let mut h = [0u8; 4];
467 self.inner
468 .read_exact(&mut h)
469 .await
470 .context("the daemon closed the connection")?;
471 self.consumed += 4;
472 let h = u32::from_le_bytes(h);
473 let tag = ((h >> 24) as u8).wrapping_sub(MPLEX_BASE);
474 let len = (h & 0x00ff_ffff) as usize;
475 if tag == MSG_DATA {
476 self.left = len;
477 continue;
478 }
479 ensure!(len <= 8192, "a {len}-byte rsync message is too long");
480 let mut body = vec![0u8; len];
481 self.inner.read_exact(&mut body).await?;
482 self.consumed += len as u64;
483 match tag {
484 MSG_ERROR_XFER | MSG_INFO | MSG_ERROR | MSG_WARNING => self
485 .messages
486 .push((tag, String::from_utf8_lossy(&body).trim_end().to_string())),
487 MSG_IO_ERROR if len == 4 => {
488 self.io_error |= i32::from_le_bytes([body[0], body[1], body[2], body[3]])
489 }
490 MSG_NOOP => {}
491 MSG_ERROR_EXIT => bail!("the daemon exited with an error"),
492 other => bail!("unexpected rsync message tag {other}"),
493 }
494 }
495 let k = (n - have).min(self.left);
496 self.inner.read_exact(&mut out[have..have + k]).await?;
497 self.consumed += k as u64;
498 have += k;
499 self.left -= k;
500 }
501 Ok(out)
502 }
503 pub async fn byte(&mut self) -> Result<u8> {
504 Ok(self.read(1).await?[0])
505 }
506 pub async fn int(&mut self) -> Result<i32> {
507 let b = self.read(4).await?;
508 Ok(i32::from_le_bytes([b[0], b[1], b[2], b[3]]))
509 }
510 pub async fn short(&mut self) -> Result<u16> {
511 let b = self.read(2).await?;
512 Ok(u16::from_le_bytes([b[0], b[1]]))
513 }
514 pub async fn longint(&mut self) -> Result<u64> {
515 let v = self.int().await?;
516 if v != -1 {
517 return Ok(v as u32 as u64);
518 }
519 let b = self.read(8).await?;
520 Ok(i64::from_le_bytes(b.try_into().unwrap_or([0; 8])) as u64)
521 }
522
523 pub async fn file_list(&mut self, r: &Request) -> Result<(Vec<Entry>, i32)> {
526 self.file_list_bounded(r, FlistLimits::DOWNLOAD).await
527 }
528
529 pub async fn file_list_bounded(
532 &mut self,
533 r: &Request,
534 limits: FlistLimits,
535 ) -> Result<(Vec<Entry>, i32)> {
536 let mut entries = Vec::new();
537 let (mut last, mut mode, mut mtime, mut uid, mut gid) =
538 (Vec::new(), 0u32, 0u32, 0u32, 0u32);
539 let mut listed = 0usize;
540 let mut name_bytes = 0usize;
541 loop {
542 let mut x = u16::from(self.byte().await?);
543 if x == 0 {
544 break;
545 }
546 listed += 1;
547 ensure!(
548 listed <= limits.max_entries,
549 "more than {} file-list entries",
550 limits.max_entries
551 );
552 if x & XMIT_EXTENDED_FLAGS != 0 {
553 x |= u16::from(self.byte().await?) << 8;
554 }
555 let l1 = if x & XMIT_SAME_NAME != 0 {
556 self.byte().await? as usize
557 } else {
558 0
559 };
560 let l2 = if x & XMIT_LONG_NAME != 0 {
561 self.int().await?.max(0) as usize
562 } else {
563 self.byte().await? as usize
564 };
565 ensure!(
566 l1 <= last.len() && l1 + l2 <= MAX_NAME,
567 "a file-list name is malformed"
568 );
569 name_bytes += l1 + l2;
570 ensure!(
571 name_bytes <= limits.max_name_bytes,
572 "more than {} bytes of file-list names",
573 limits.max_name_bytes
574 );
575 let mut name = last[..l1].to_vec();
576 name.extend(self.read(l2).await?);
577 last = name.clone();
578 let size = self.longint().await?;
579 if x & XMIT_SAME_TIME == 0 {
580 mtime = self.int().await? as u32;
581 }
582 if x & XMIT_SAME_MODE == 0 {
583 mode = self.int().await? as u32;
584 }
585 if r.owner && x & XMIT_SAME_UID == 0 {
586 uid = self.int().await? as u32;
587 }
588 if r.group && x & XMIT_SAME_GID == 0 {
589 gid = self.int().await? as u32;
590 }
591 let kind = Kind::of_mode(mode);
592 let mut data = Vec::new();
593 if r.links && kind == Some(Kind::Symlink) {
594 let n = self.int().await?;
595 ensure!(
596 (0..=MAX_NAME as i32).contains(&n),
597 "a symlink target is too long"
598 );
599 name_bytes += n as usize;
600 ensure!(
601 name_bytes <= limits.max_name_bytes,
602 "more than {} bytes of file-list names",
603 limits.max_name_bytes
604 );
605 data = self.read(n as usize).await?;
606 }
607 ensure!(
608 kind.is_some() || !r.devices,
609 "a device or special file in the list; -D is not supported for uploads"
610 );
611 if r.checksum && kind == Some(Kind::File) {
612 self.read(16).await?;
613 }
614 let path = String::from_utf8_lossy(&name).to_string();
615 ensure!(
616 !path.starts_with('/') && !path.split('/').any(|c| c == ".."),
617 "unsafe file-list name {path:?}"
618 );
619 if let Some(kind) = kind {
621 entries.push(Entry {
622 path,
623 kind,
624 data,
625 mode,
626 mtime,
627 uid,
628 gid,
629 size,
630 });
631 }
632 }
633 for wanted in [r.owner, r.group] {
634 if wanted && !r.numeric_ids {
635 loop {
636 let id = self.int().await?;
637 if id == 0 {
638 break;
639 }
640 let n = self.byte().await?;
641 self.read(n as usize).await?;
642 }
643 }
644 }
645 let io_error = self.int().await?;
646 Ok((entries, io_error))
647 }
648}
649
650pub fn entry_json(e: &Entry) -> Value {
652 let mut v = json!({
653 "path": e.path,
654 "type": e.kind.name(),
655 "size": e.size,
656 "mode": format!("{:o}", e.mode & 0o7777),
657 "mtime": e.mtime,
658 });
659 if e.kind == Kind::Symlink && !e.data.is_empty() {
660 v["target"] = json!(String::from_utf8_lossy(&e.data));
661 }
662 v
663}
664
665pub fn content_json(data: &[u8]) -> (String, &'static str) {
667 match std::str::from_utf8(data) {
668 Ok(s)
669 if s.chars()
670 .all(|c| !c.is_control() || matches!(c, '\n' | '\r' | '\t')) =>
671 {
672 (s.to_string(), "utf8")
673 }
674 _ => (hex::encode(data), "hex"),
675 }
676}
677
678pub fn entry_from_json(v: &Value, default_mtime: u32) -> Result<Entry> {
680 let path = v["path"]
681 .as_str()
682 .context("each entry needs a path")?
683 .trim_matches('/')
684 .to_string();
685 ensure!(
686 !path.is_empty() && path.len() <= MAX_NAME,
687 "an entry path is empty or too long"
688 );
689 ensure!(
690 !path
691 .split('/')
692 .any(|c| c.is_empty() || c == "." || c == ".."),
693 "entry path {path:?} must be relative, without . or .. components"
694 );
695 let kind = match v["type"].as_str().unwrap_or("file") {
696 "file" => Kind::File,
697 "dir" | "directory" => Kind::Dir,
698 "symlink" | "link" => Kind::Symlink,
699 other => bail!("entry type {other:?} is not file, dir or symlink"),
700 };
701 let data = match kind {
702 Kind::File => {
703 let content = v["content"].as_str().unwrap_or_default();
704 match v["encoding"].as_str().unwrap_or("utf8") {
705 "utf8" => content.as_bytes().to_vec(),
706 "hex" => hex::decode(content).context("content is not hex")?,
707 other => bail!("encoding {other:?} is not utf8 or hex"),
708 }
709 }
710 Kind::Symlink => v["target"]
711 .as_str()
712 .context("a symlink needs a target")?
713 .as_bytes()
714 .to_vec(),
715 Kind::Dir => vec![],
716 };
717 let type_bits = match kind {
718 Kind::File => S_IFREG,
719 Kind::Dir => S_IFDIR,
720 Kind::Symlink => S_IFLNK,
721 };
722 let perms = match v.get("mode") {
723 None | Some(Value::Null) => match kind {
724 Kind::File => 0o644,
725 Kind::Dir => 0o755,
726 Kind::Symlink => 0o777,
727 },
728 Some(Value::String(s)) => u32::from_str_radix(s.trim_start_matches("0o"), 8)
729 .with_context(|| format!("mode {s:?} is not octal"))?,
730 Some(n) => n
731 .as_u64()
732 .context("mode is an octal string such as \"644\"")? as u32,
733 };
734 ensure!(
735 perms <= 0o7777,
736 "mode {perms:o} has bits beyond permissions"
737 );
738 let mtime = v["mtime"]
739 .as_u64()
740 .map(|t| t as u32)
741 .unwrap_or(default_mtime);
742 let size = match kind {
743 Kind::Dir => 4096,
744 _ => data.len() as u64,
745 };
746 Ok(Entry {
747 path,
748 kind,
749 data,
750 mode: type_bits | perms,
751 mtime,
752 uid: v["uid"].as_u64().unwrap_or(0) as u32,
753 gid: v["gid"].as_u64().unwrap_or(0) as u32,
754 size,
755 })
756}