use std::fmt;
use std::hash::{BuildHasher, Hasher};
use std::net::SocketAddr;
pub const DEFAULT_PORT: u16 = 47710;
const ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct Key(String);
impl Key {
pub fn draw() -> Key {
let bits = random_u64();
let chars: String = (0..8)
.map(|i| ALPHABET[((bits >> (i * 5)) & 31) as usize] as char)
.collect();
Key(chars)
}
pub fn parse(text: &str) -> Option<Key> {
let mut out = String::new();
for c in text.chars() {
let c = match c.to_ascii_uppercase() {
'-' => continue,
'O' => '0',
'I' | 'L' => '1',
c => c,
};
if !c.is_ascii() || !ALPHABET.contains(&(c as u8)) {
return None;
}
out.push(c);
}
(out.len() == 8).then_some(Key(out))
}
}
impl fmt::Display for Key {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}-{}", &self.0[..4], &self.0[4..])
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Token(String);
impl Token {
pub fn draw() -> Token {
let mut out = String::with_capacity(26);
for _ in 0..2 {
let bits = random_u64();
for i in 0..12 {
out.push(ALPHABET[((bits >> (i * 5)) & 31) as usize] as char);
}
}
Token(out)
}
pub fn from_wire(text: &str) -> Token {
Token(text.to_string())
}
pub fn reveal(&self) -> &str {
&self.0
}
}
impl fmt::Debug for Token {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("Token(..)")
}
}
fn random_u64() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let mut h = std::collections::hash_map::RandomState::new().build_hasher();
h.write_u64(COUNTER.fetch_add(1, Ordering::Relaxed));
h.write_u128(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos()),
);
h.finish()
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ConnString {
pub tls: bool,
pub host: String,
pub port: u16,
pub key: Option<Key>,
}
impl ConnString {
pub fn new(host: impl Into<String>, port: u16, key: Option<Key>) -> ConnString {
ConnString {
tls: false,
host: host.into(),
port,
key,
}
}
pub fn parse(text: &str) -> Result<ConnString, String> {
let text = text.trim();
let (tls, rest) = if let Some(rest) = text.strip_prefix("pinch://") {
(false, rest)
} else if let Some(rest) = text.strip_prefix("pinchs://") {
(true, rest)
} else {
return Err("a connection string starts with pinch:// or pinchs://".to_string());
};
let (authority, key) = match rest.split_once('/') {
Some((authority, "")) => (authority, None),
Some((authority, key)) => (
authority,
Some(Key::parse(key).ok_or_else(|| format!("{key:?} is not a join key"))?),
),
None => (rest, None),
};
let (host, port) = match authority.rsplit_once(':') {
Some((host, port)) if !port.contains(']') => (
host,
port.parse::<u16>()
.map_err(|_| format!("{port:?} is not a port"))?,
),
_ => (authority, DEFAULT_PORT),
};
let host = host.trim_start_matches('[').trim_end_matches(']');
if host.is_empty() {
return Err("the connection string names no host".to_string());
}
Ok(ConnString {
tls,
host: host.to_string(),
port,
key,
})
}
}
impl fmt::Display for ConnString {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let scheme = if self.tls { "pinchs" } else { "pinch" };
let host = if self.host.contains(':') {
format!("[{}]", self.host)
} else {
self.host.clone()
};
write!(f, "{scheme}://{host}:{}", self.port)?;
if let Some(key) = &self.key {
write!(f, "/{key}")?;
}
Ok(())
}
}
pub fn reachable_host(bound: SocketAddr) -> String {
if !bound.ip().is_unspecified() {
return bound.ip().to_string();
}
std::net::UdpSocket::bind("0.0.0.0:0")
.and_then(|socket| {
socket.connect("192.0.2.1:9")?;
socket.local_addr()
})
.map_or_else(|_| "127.0.0.1".to_string(), |addr| addr.ip().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_connection_string_reads_back_as_it_was_written() {
let s = ConnString::new("127.0.0.1", 47710, Some(Key::draw()));
assert_eq!(ConnString::parse(&s.to_string()), Ok(s.clone()));
let open = ConnString::new("192.168.1.20", 47710, None);
assert_eq!(open.to_string(), "pinch://192.168.1.20:47710");
assert_eq!(ConnString::parse("pinch://192.168.1.20:47710/"), Ok(open));
}
#[test]
fn the_secure_form_and_a_missing_port_are_understood() {
let s = ConnString::parse("pinchs://cup.example.org/7f3k-9qxa").expect("parses");
assert!(s.tls);
assert_eq!(s.port, DEFAULT_PORT);
assert_eq!(s.key.map(|k| k.to_string()), Some("7F3K-9QXA".to_string()));
assert_eq!(
ConnString::parse("pinch://[::1]:5000").map(|s| s.host),
Ok("::1".to_string())
);
}
#[test]
fn garbage_is_refused_with_a_reason() {
assert!(ConnString::parse("http://x").is_err());
assert!(ConnString::parse("pinch://:47710").is_err());
assert!(ConnString::parse("pinch://h:port").is_err());
assert!(ConnString::parse("pinch://h:1/NOTAKEY!").is_err());
}
#[test]
fn a_key_is_forgiving_to_read_and_strict_about_length() {
let key = Key::draw();
let shown = key.to_string();
assert_eq!(shown.len(), 9);
assert_eq!(Key::parse(&shown.to_lowercase()), Some(key));
assert_eq!(Key::parse("7F3K-9QXO"), Key::parse("7F3K-9QX0"));
assert_eq!(Key::parse("7F3K"), None);
}
#[test]
fn keys_and_tokens_are_fresh_every_time() {
assert_ne!(Key::draw(), Key::draw());
assert_ne!(Token::draw(), Token::draw());
assert_eq!(format!("{:?}", Token::draw()), "Token(..)");
}
}