Skip to main content

isb_core/
sftp.rs

1//! Just enough SFTP (version 3) to stat, chown and chmod a path inside an
2//! instance through incus' `/1.0/instances/NAME/sftp`.
3//!
4//! incusd serves it from inside the instance's own namespaces (forkfile, or
5//! the agent in a VM), so ids are the guest's own whatever the idmap, and the
6//! guest needs no shell or tool at all.
7
8use std::io::{Read, Write};
9use std::os::unix::net::UnixStream;
10use std::time::Duration;
11
12use crate::client::{Client, encode_segment};
13use crate::error::{Error, Result};
14
15const INIT: u8 = 1;
16const VERSION: u8 = 2;
17const SETSTAT: u8 = 9;
18const STAT: u8 = 17;
19const STATUS: u8 = 101;
20const ATTRS: u8 = 105;
21const ATTR_SIZE: u32 = 0x1;
22const ATTR_UIDGID: u32 = 0x2;
23const ATTR_PERMISSIONS: u32 = 0x4;
24#[cfg(test)]
25const ATTR_ACMODTIME: u32 = 0x8;
26const NO_SUCH_FILE: u32 = 2;
27
28/// Owner and mode of a path.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct Stat {
31    pub uid: u32,
32    pub gid: u32,
33    /// Permission bits (`0o7777`), the file type masked off.
34    pub mode: u32,
35}
36
37/// One SFTP session with an instance.
38pub struct Sftp {
39    stream: UnixStream,
40    next_id: u32,
41    what: String,
42}
43
44impl Sftp {
45    /// Open a session; every request then waits at most `timeout`.
46    pub fn open(client: &Client, instance: &str, timeout: Duration) -> Result<Self> {
47        let path = format!("/1.0/instances/{}/sftp", encode_segment(instance));
48        let stream = client.upgrade(&path, "sftp", timeout)?;
49        let mut s = Sftp {
50            stream,
51            next_id: 1,
52            what: format!("sftp to {instance}"),
53        };
54        s.send(INIT, &3u32.to_be_bytes())?;
55        let (t, _) = s.recv()?;
56        if t != VERSION {
57            return Err(s.protocol(format!("expected VERSION, got packet type {t}")));
58        }
59        Ok(s)
60    }
61
62    /// stat (following symlinks); `None` if nothing is there.
63    pub fn stat(&mut self, path: &str) -> Result<Option<Stat>> {
64        let id = self.request(STAT, path, &[])?;
65        let (t, body) = self.recv()?;
66        let mut r = Reader::new(&body);
67        if r.u32()? != id {
68            return Err(self.protocol("reply to another request".into()));
69        }
70        match t {
71            ATTRS => {
72                let flags = r.u32()?;
73                if flags & ATTR_SIZE != 0 {
74                    r.u64()?;
75                }
76                if flags & ATTR_UIDGID == 0 || flags & ATTR_PERMISSIONS == 0 {
77                    return Err(self.protocol(format!("stat {path}: no owner or mode")));
78                }
79                let (uid, gid) = (r.u32()?, r.u32()?);
80                let mode = r.u32()? & 0o7777;
81                Ok(Some(Stat { uid, gid, mode }))
82            }
83            STATUS => match r.u32()? {
84                NO_SUCH_FILE => Ok(None),
85                code => Err(self.failed(&format!("stat {path}"), code, &mut r)),
86            },
87            t => Err(self.protocol(format!("unexpected packet type {t}"))),
88        }
89    }
90
91    /// chown (following symlinks).
92    pub fn chown(&mut self, path: &str, uid: u32, gid: u32) -> Result<()> {
93        let mut a = ATTR_UIDGID.to_be_bytes().to_vec();
94        a.extend(uid.to_be_bytes());
95        a.extend(gid.to_be_bytes());
96        self.setstat(path, &a, &format!("chown {uid}:{gid} {path}"))
97    }
98
99    /// chmod to `mode` (`0o7777` bits).
100    pub fn chmod(&mut self, path: &str, mode: u32) -> Result<()> {
101        let mut a = ATTR_PERMISSIONS.to_be_bytes().to_vec();
102        a.extend((mode & 0o7777).to_be_bytes());
103        self.setstat(path, &a, &format!("chmod {mode:04o} {path}"))
104    }
105
106    fn setstat(&mut self, path: &str, attrs: &[u8], what: &str) -> Result<()> {
107        let id = self.request(SETSTAT, path, attrs)?;
108        let (t, body) = self.recv()?;
109        let mut r = Reader::new(&body);
110        if t != STATUS || r.u32()? != id {
111            return Err(self.protocol(format!("{what}: unexpected reply type {t}")));
112        }
113        match r.u32()? {
114            0 => Ok(()),
115            code => Err(self.failed(what, code, &mut r)),
116        }
117    }
118
119    fn request(&mut self, kind: u8, path: &str, rest: &[u8]) -> Result<u32> {
120        let id = self.next_id;
121        self.next_id += 1;
122        let mut body = id.to_be_bytes().to_vec();
123        body.extend((path.len() as u32).to_be_bytes());
124        body.extend(path.as_bytes());
125        body.extend(rest);
126        self.send(kind, &body)?;
127        Ok(id)
128    }
129
130    fn send(&mut self, kind: u8, body: &[u8]) -> Result<()> {
131        let mut p = ((body.len() + 1) as u32).to_be_bytes().to_vec();
132        p.push(kind);
133        p.extend(body);
134        self.stream.write_all(&p).map_err(|e| self.io(e))
135    }
136
137    fn recv(&mut self) -> Result<(u8, Vec<u8>)> {
138        let mut len = [0u8; 4];
139        self.stream.read_exact(&mut len).map_err(|e| self.io(e))?;
140        let len = u32::from_be_bytes(len) as usize;
141        if len == 0 || len > 1 << 20 {
142            return Err(self.protocol(format!("packet of {len} bytes")));
143        }
144        let mut p = vec![0u8; len];
145        self.stream.read_exact(&mut p).map_err(|e| self.io(e))?;
146        Ok((p[0], p.split_off(1)))
147    }
148
149    fn failed(&self, what: &str, code: u32, r: &mut Reader) -> Error {
150        let msg = r.string().unwrap_or_default();
151        Error::OperationFailed {
152            step: format!("{} ({what})", self.what),
153            message: if msg.is_empty() {
154                format!("SFTP status {code}")
155            } else {
156                msg
157            },
158        }
159    }
160
161    fn protocol(&self, m: String) -> Error {
162        Error::Protocol(format!("{}: {m}", self.what))
163    }
164
165    fn io(&self, e: std::io::Error) -> Error {
166        Error::Protocol(format!("{}: {e}", self.what))
167    }
168}
169
170struct Reader<'a> {
171    b: &'a [u8],
172}
173
174impl<'a> Reader<'a> {
175    fn new(b: &'a [u8]) -> Self {
176        Reader { b }
177    }
178
179    fn take(&mut self, n: usize) -> Result<&'a [u8]> {
180        if self.b.len() < n {
181            return Err(Error::Protocol("sftp: short packet".into()));
182        }
183        let (h, t) = self.b.split_at(n);
184        self.b = t;
185        Ok(h)
186    }
187
188    fn u32(&mut self) -> Result<u32> {
189        let b = self.take(4)?;
190        Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
191    }
192
193    fn u64(&mut self) -> Result<u64> {
194        Ok(((self.u32()? as u64) << 32) | self.u32()? as u64)
195    }
196
197    fn string(&mut self) -> Result<String> {
198        let n = self.u32()? as usize;
199        Ok(String::from_utf8_lossy(self.take(n)?).into_owned())
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn reads_attrs_as_incus_sends_them() {
209        // An ATTRS reply captured from incus 7.5 (size, uid/gid, mode, times).
210        let body = b"\x00\x00\x00\x01\x00\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x10\x00\
211            \x00\x00\x03\xe8\x00\x00\x03\xe9\x00\x00\x41\xc9\x6a\xc3\x3f\x51\x6a\xc3\x3f\x51";
212        let mut r = Reader::new(body);
213        assert_eq!(r.u32().unwrap(), 1);
214        let flags = r.u32().unwrap();
215        assert_eq!(
216            flags,
217            ATTR_SIZE | ATTR_UIDGID | ATTR_PERMISSIONS | ATTR_ACMODTIME
218        );
219        assert_eq!(r.u64().unwrap(), 4096);
220        assert_eq!((r.u32().unwrap(), r.u32().unwrap()), (1000, 1001));
221        assert_eq!(r.u32().unwrap() & 0o7777, 0o711);
222        assert!(Reader::new(b"\x00\x00").u32().is_err());
223    }
224}