Skip to main content

liminal_server/server/connection/
declared_holder.rs

1//! ACCESS-012 R2: who a connection on an open exchange says it is.
2//!
3//! An exchange with no `[auth]` proves nothing about a connection; it can
4//! still record what the connection declares, so the services a seat creates
5//! carry a name and a list says whose they are. The record is `open:<name>`
6//! on the `Connect` frame; any other record, an empty one and a Lys pass
7//! record included, is admitted as `open:anonymous`. A gated exchange never
8//! reads this: its `Connect` refuses the record as an unknown credential.
9
10/// The prefix of the record an open seat sends and of every holder it is
11/// recorded as.
12pub const PREFIX: &str = "open:";
13
14/// The name of a connection that declared none.
15pub const ANONYMOUS: &str = "anonymous";
16
17/// What an open-access connection declared at `Connect`.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum DeclaredHolder {
20    /// `open:<name>`, the name as the record gave it, or anonymous.
21    Open(String),
22}
23
24impl DeclaredHolder {
25    /// Reads the record an open exchange admitted: `open:<name>` with a name
26    /// of 1 to 64 of `a-z`, `0-9`, `.`, `_` or `-`; anything else is
27    /// anonymous.
28    #[must_use]
29    pub fn from_record(record: &[u8]) -> Self {
30        let name = std::str::from_utf8(record)
31            .ok()
32            .and_then(|text| text.strip_prefix(PREFIX))
33            .filter(|name| is_name(name));
34        Self::Open(name.unwrap_or(ANONYMOUS).to_owned())
35    }
36
37    /// The name alone: the declared name, or `anonymous`.
38    #[must_use]
39    pub fn name(&self) -> &str {
40        match self {
41            Self::Open(name) => name,
42        }
43    }
44
45    /// The holder as every record names it: `open:<name>`.
46    #[must_use]
47    pub fn record(&self) -> String {
48        match self {
49            Self::Open(name) => format!("{PREFIX}{name}"),
50        }
51    }
52}
53
54/// Whether `name` is a declared name: 1 to 64 of `a-z`, `0-9`, `.`, `_` or `-`.
55#[must_use]
56pub fn is_name(name: &str) -> bool {
57    (1..=64).contains(&name.len())
58        && name.bytes().all(|b| {
59            b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'-')
60        })
61}
62
63#[cfg(test)]
64mod tests {
65    use super::{DeclaredHolder, is_name};
66
67    #[test]
68    fn a_record_names_its_holder_or_is_anonymous() {
69        assert_eq!(
70            DeclaredHolder::from_record(b"open:hermes").record(),
71            "open:hermes"
72        );
73        for record in [
74            &b""[..],
75            b"open:",
76            b"open:Hermes",
77            b"hermes",
78            b"\xff",
79            b"open:a b",
80        ] {
81            assert_eq!(
82                DeclaredHolder::from_record(record).record(),
83                "open:anonymous",
84                "{record:?}"
85            );
86        }
87    }
88
89    #[test]
90    fn a_name_is_one_to_sixty_four_of_the_allowed_bytes() {
91        assert!(is_name("hermes") && is_name("a.b_c-d9") && is_name(&"x".repeat(64)));
92        assert!(!is_name("") && !is_name("Hermes") && !is_name("a/b") && !is_name(&"x".repeat(65)));
93    }
94}