superfluid_daemon/
fleet_token.rs1use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6
7pub fn path(home: &Path) -> PathBuf {
8 home.join("fleet").join("token")
9}
10
11pub 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
18pub 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
26fn 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
49pub 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 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}