Compare commits
1
Commits
f95bc77f7b
...
d35c880753
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d35c880753 |
No files matched your search
+1
-4
@@ -155,10 +155,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn addresses() -> Vec<IpAddr> {
|
||||
vec![
|
||||
"192.168.1.5".parse().unwrap(),
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
]
|
||||
vec!["192.168.1.5".parse().unwrap(), "127.0.0.1".parse().unwrap()]
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -24,7 +24,10 @@
|
||||
//! middleware, which stays in each project because it is generic over
|
||||
//! that project's state.
|
||||
|
||||
use std::fs;
|
||||
use std::net::IpAddr;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use base64::Engine;
|
||||
@@ -97,6 +100,74 @@ pub fn print_enrollment(scheme: &str, host: IpAddr, port: u16, token: &str) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How long a spooled enrolment stays valid unused. The link is meant to be
|
||||
/// opened straight away, by the tool that asked for it; one that was never
|
||||
/// opened should not stay a valid credential on disk.
|
||||
pub const PENDING_TTL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
/// Records a token minted by another process for the running server to
|
||||
/// adopt on first use -- see [`take_pending`].
|
||||
///
|
||||
/// Why a spool rather than writing the config: the server holds its config
|
||||
/// in memory and writes it back whole, so a second process appending a
|
||||
/// token to the file loses the race with the next save, silently. Here the
|
||||
/// other process writes only into `dir` (created private to the user), one
|
||||
/// file per token, named by the hash and holding the device name; the
|
||||
/// server owns the config as before and moves the entry across itself.
|
||||
/// Only the hash touches disk, as with every stored token.
|
||||
pub fn spool_pending(dir: &Path, name: &str, token: &str) -> Result<()> {
|
||||
fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(dir, fs::Permissions::from_mode(0o700))
|
||||
.with_context(|| format!("restrict {}", dir.display()))?;
|
||||
}
|
||||
let path = dir.join(token_hash_hex(token));
|
||||
fs::write(&path, name).with_context(|| format!("write {}", path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adopts a spooled token if `presented` is one: returns the device name
|
||||
/// it was spooled under and removes the entry, so a spooled token is
|
||||
/// consumed exactly once and belongs to the config from then on. Anything
|
||||
/// older than [`PENDING_TTL`] is removed rather than honoured.
|
||||
///
|
||||
/// A missing directory is the common case -- nothing has ever been
|
||||
/// spooled -- and answers `None` like an empty one.
|
||||
pub fn take_pending(dir: &Path, presented: &str) -> Result<Option<String>> {
|
||||
let entries = match fs::read_dir(dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(error) => return Err(error).with_context(|| format!("read {}", dir.display())),
|
||||
};
|
||||
let wanted = token_hash_hex(presented);
|
||||
let mut found = None;
|
||||
for entry in entries {
|
||||
let entry = entry.with_context(|| format!("read {}", dir.display()))?;
|
||||
let path = entry.path();
|
||||
let fresh = entry
|
||||
.metadata()
|
||||
.and_then(|meta| meta.modified())
|
||||
.ok()
|
||||
.and_then(|modified| modified.elapsed().ok())
|
||||
.is_some_and(|age| age < PENDING_TTL);
|
||||
if !fresh {
|
||||
let _ = fs::remove_file(&path);
|
||||
continue;
|
||||
}
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if bool::from(name.as_bytes().ct_eq(wanted.as_bytes())) {
|
||||
found = Some(path);
|
||||
}
|
||||
}
|
||||
let Some(path) = found else { return Ok(None) };
|
||||
let device = fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
|
||||
fs::remove_file(&path).with_context(|| format!("remove {}", path.display()))?;
|
||||
Ok(Some(device))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -143,4 +214,56 @@ mod tests {
|
||||
let other = enrollment_uri("aiapp", "10.66.0.1".parse().unwrap(), 8443, "tok");
|
||||
assert!(other.starts_with("aiapp://enroll?"));
|
||||
}
|
||||
|
||||
fn scratch_dir() -> std::path::PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("wg-app-link-enroll-{}", generate_token()));
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
/// A spooled token is adopted once, under the name it was spooled with,
|
||||
/// and by nothing but that token.
|
||||
#[test]
|
||||
fn a_spooled_token_is_taken_exactly_once() {
|
||||
let dir = scratch_dir();
|
||||
let token = generate_token();
|
||||
spool_pending(&dir, "tablet", &token).unwrap();
|
||||
assert_eq!(take_pending(&dir, "wrong").unwrap(), None);
|
||||
assert_eq!(
|
||||
take_pending(&dir, &token).unwrap().as_deref(),
|
||||
Some("tablet")
|
||||
);
|
||||
assert_eq!(take_pending(&dir, &token).unwrap(), None, "consumed");
|
||||
assert!(
|
||||
fs::read_dir(&dir).unwrap().next().is_none(),
|
||||
"nothing left behind"
|
||||
);
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
/// Nothing spooled -- not even the directory -- is an ordinary miss.
|
||||
#[test]
|
||||
fn no_spool_is_a_miss() {
|
||||
let dir = scratch_dir().join("never-made");
|
||||
assert_eq!(take_pending(&dir, "anything").unwrap(), None);
|
||||
}
|
||||
|
||||
/// An entry past its age is swept rather than honoured.
|
||||
#[test]
|
||||
fn a_stale_entry_is_swept_not_honoured() {
|
||||
let dir = scratch_dir();
|
||||
let token = generate_token();
|
||||
spool_pending(&dir, "old", &token).unwrap();
|
||||
let path = dir.join(token_hash_hex(&token));
|
||||
let past = std::time::SystemTime::now() - PENDING_TTL - Duration::from_secs(1);
|
||||
fs::File::options()
|
||||
.write(true)
|
||||
.open(&path)
|
||||
.unwrap()
|
||||
.set_modified(past)
|
||||
.unwrap();
|
||||
assert_eq!(take_pending(&dir, &token).unwrap(), None);
|
||||
assert!(!path.exists(), "swept");
|
||||
fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user