Skip to main content

superfluid_daemon/
fleet_token.rs

1//! `superfluid fleet token`: the secret a head and its nodes authenticate and encrypt their
2//! link with, kept at `$SUPERFLUID_HOME/fleet/token`.
3
4use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6
7pub fn path(home: &Path) -> PathBuf {
8    home.join("fleet").join("token")
9}
10
11/// The token at `home`, if one has been made.
12pub fn read(home: &Path) -> Option<Vec<u8>> {
13    let t = std::fs::read(path(home)).ok()?;
14    let t = t.trim_ascii().to_vec();
15    (!t.is_empty()).then_some(t)
16}
17
18/// Keeps `token` at `home`, owner-readable, replacing one that differs.
19pub fn write(home: &Path, token: &[u8]) -> Result<(), String> {
20    if read(home).as_deref() == Some(token) {
21        return Ok(());
22    }
23    save(home, token, false).map(|_| ())
24}
25
26/// Writes the token file, owner-readable; `fresh` refuses to replace one that exists, and
27/// says so with `false`.
28fn save(home: &Path, token: &[u8], fresh: bool) -> Result<bool, String> {
29    let p = path(home);
30    let dir = p.parent().expect("the token has a directory");
31    std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
32    use std::os::unix::fs::OpenOptionsExt;
33    let mut opts = std::fs::OpenOptions::new();
34    opts.write(true).mode(0o600);
35    if fresh {
36        opts.create_new(true);
37    } else {
38        opts.create(true).truncate(true);
39    }
40    let mut f = match opts.open(&p) {
41        Ok(f) => f,
42        Err(e) if fresh && e.kind() == std::io::ErrorKind::AlreadyExists => return Ok(false),
43        Err(e) => return Err(format!("{}: {e}", p.display())),
44    };
45    f.write_all(token).map_err(|e| format!("{}: {e}", p.display()))?;
46    Ok(true)
47}
48
49/// The token at `home`, made now (32 random bytes as hex, readable by the owner only) if absent.
50pub fn read_or_create(home: &Path) -> Result<Vec<u8>, String> {
51    if let Some(t) = read(home) {
52        return Ok(t);
53    }
54    let mut raw = [0u8; 32];
55    std::fs::File::open("/dev/urandom")
56        .and_then(|mut f| f.read_exact(&mut raw))
57        .map_err(|e| format!("/dev/urandom: {e}"))?;
58    let token: String = raw.iter().map(|b| format!("{b:02x}")).collect();
59    // Made by another process in the meantime: theirs is the token.
60    if !save(home, token.as_bytes(), true)? {
61        return read(home).ok_or_else(|| format!("{}: made by another process but empty", path(home).display()));
62    }
63    Ok(token.into_bytes())
64}
65
66pub const HELP: &str = "The fleet's shared token.
67Usage: superfluid fleet token          Print the token, making it the first time
68  The token lives at $SUPERFLUID_HOME/fleet/token. `serve --fleet` presents it to every
69  node and `superfluid-noded` expects it (both read that path by default), and the link
70  between them is encrypted with it. Copy the file, or the printed value, to each node.
71";
72
73pub fn run(args: &[String]) {
74    match args.first().map(String::as_str) {
75        Some("token") => {
76            let home = crate::runtimes::default_home();
77            match read_or_create(&home) {
78                Ok(t) => {
79                    println!("{}", String::from_utf8_lossy(&t));
80                    eprintln!("superfluid: fleet token at {}", path(&home).display());
81                }
82                Err(e) => {
83                    eprintln!("superfluid: {e}");
84                    std::process::exit(1);
85                }
86            }
87        }
88        _ => {
89            eprint!("{HELP}");
90            std::process::exit(2);
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn a_token_is_made_once_and_read_back() {
101        let home = std::env::temp_dir().join(format!("superfluid-fleet-token-{}", std::process::id()));
102        let _ = std::fs::remove_dir_all(&home);
103        assert_eq!(read(&home), None);
104        let first = read_or_create(&home).unwrap();
105        assert_eq!(first.len(), 64);
106        assert!(first.iter().all(|b| b.is_ascii_hexdigit()));
107        assert_eq!(read_or_create(&home).unwrap(), first);
108        assert_eq!(read(&home).unwrap(), first);
109        use std::os::unix::fs::PermissionsExt;
110        assert_eq!(std::fs::metadata(path(&home)).unwrap().permissions().mode() & 0o777, 0o600);
111        std::fs::remove_dir_all(&home).ok();
112    }
113}