use crate::error::{Error, Result};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
pub fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
let mut diff = (a.len() ^ b.len()) as u8;
for i in 0..a.len().max(b.len()) {
diff |= a.get(i).copied().unwrap_or(0) ^ b.get(i).copied().unwrap_or(0);
}
std::hint::black_box(diff) == 0
}
pub fn random_bytes(len: usize) -> Result<Vec<u8>> {
let mut buf = vec![0u8; len];
getrandom::fill(&mut buf)
.map_err(|e| Error::internal(format!("Random number generator unavailable: {}", e)))?;
Ok(buf)
}
pub fn random_token(bytes: usize) -> Result<String> {
let mut out = String::with_capacity(bytes * 2);
for b in random_bytes(bytes)? {
out.push(char::from_digit(u32::from(b >> 4), 16).unwrap_or('0'));
out.push(char::from_digit(u32::from(b & 0xf), 16).unwrap_or('0'));
}
Ok(out)
}
pub fn random_alphanumeric(len: usize) -> Result<String> {
const ALPHABET: &[u8; 62] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let mut out = String::with_capacity(len);
while out.len() < len {
for b in random_bytes(len.max(8))? {
if b < 248 && out.len() < len {
out.push(ALPHABET[usize::from(b) % 62] as char);
}
}
}
Ok(out)
}
pub fn is_public_ip(ip: IpAddr) -> bool {
fn public_v4(v4: Ipv4Addr) -> bool {
let o = v4.octets();
!(v4.is_loopback()
|| v4.is_private()
|| v4.is_link_local()
|| v4.is_unspecified()
|| v4.is_broadcast()
|| v4.is_multicast()
|| v4.is_documentation()
|| o[0] == 0 || o[0] >= 240 || (o[0] == 100 && (64..128).contains(&o[1])) || (o[0] == 198 && (o[1] == 18 || o[1] == 19)) || (o[0] == 192 && o[1] == 0 && o[2] == 0)) }
match ip {
IpAddr::V4(v4) => public_v4(v4),
IpAddr::V6(v6) => {
let seg = v6.segments();
let embedded =
|a: u16, b: u16| Ipv4Addr::new((a >> 8) as u8, a as u8, (b >> 8) as u8, b as u8);
if let Some(v4) = v6.to_ipv4_mapped() {
return public_v4(v4);
}
if seg[..6] == [0x64, 0xff9b, 0, 0, 0, 0] {
return public_v4(embedded(seg[6], seg[7]));
}
if seg[0] == 0x2002 {
return public_v4(embedded(seg[1], seg[2]));
}
if seg[..6] == [0, 0, 0, 0, 0, 0] {
return false;
}
!(v6.is_loopback()
|| v6.is_unspecified()
|| v6.is_multicast()
|| (seg[0] & 0xfe00) == 0xfc00 || (seg[0] & 0xffc0) == 0xfe80 || (seg[0] == 0x2001 && seg[1] == 0x0db8)) }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResolveError {
Resolution,
NotPublic(IpAddr),
}
impl std::fmt::Display for ResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResolveError::Resolution => f.write_str("name resolution failed"),
ResolveError::NotPublic(ip) => write!(f, "target address {} is not public", ip),
}
}
}
impl std::error::Error for ResolveError {}
pub async fn resolve_public(
host: &str,
port: u16,
allow_private: bool,
) -> std::result::Result<SocketAddr, ResolveError> {
let addrs: Vec<SocketAddr> = tokio::net::lookup_host((host, port))
.await
.map_err(|_| ResolveError::Resolution)?
.collect();
let first = addrs.first().copied().ok_or(ResolveError::Resolution)?;
if !allow_private {
if let Some(bad) = addrs.iter().find(|a| !is_public_ip(a.ip())) {
return Err(ResolveError::NotPublic(bad.ip()));
}
}
Ok(first)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn constant_time_eq_semantics() {
assert!(constant_time_eq(b"", b""));
assert!(constant_time_eq(b"secret", b"secret"));
assert!(!constant_time_eq(b"secret", b"secreT"));
assert!(!constant_time_eq(b"secret", b"secre"));
assert!(!constant_time_eq(b"", b"x"));
assert!(!constant_time_eq(b"abc", b"abc\0"));
}
#[test]
fn public_and_non_public_addresses() {
let ip = |s: &str| s.parse::<IpAddr>().unwrap();
for public in [
"8.8.8.8",
"1.1.1.1",
"93.184.216.34",
"172.32.0.1",
"100.63.255.255",
"100.128.0.1",
"198.17.0.1",
"198.20.0.1",
"2606:4700:4700::1111",
"2a00:1450:4001::1",
"64:ff9b::808:808",
"2002:0808:0808::1",
"::ffff:8.8.8.8",
] {
assert!(is_public_ip(ip(public)), "{public} should be public");
}
for private in [
"127.0.0.1",
"127.255.255.254",
"10.0.0.1",
"172.16.0.1",
"172.31.255.255",
"192.168.1.1",
"169.254.169.254",
"0.0.0.0",
"0.1.2.3",
"255.255.255.255",
"100.64.0.1",
"100.127.255.255",
"224.0.0.1",
"239.255.255.255",
"240.0.0.1",
"192.0.2.1",
"198.51.100.1",
"203.0.113.1",
"198.18.0.1",
"198.19.255.255",
"192.0.0.1",
"::1",
"::",
"::1234",
"fe80::1",
"fd00::1",
"fc00::1",
"ff02::1",
"2001:db8::1",
"::ffff:127.0.0.1",
"::ffff:10.0.0.1",
"::ffff:169.254.169.254",
"64:ff9b::7f00:1",
"64:ff9b::a00:1",
"2002:7f00:1::1",
"2002:c0a8:101::1",
] {
assert!(!is_public_ip(ip(private)), "{private} should not be public");
}
}
#[tokio::test]
async fn resolve_public_vets_literals_and_names() {
assert_eq!(
resolve_public("93.184.216.34", 443, false).await,
Ok("93.184.216.34:443".parse().unwrap())
);
assert_eq!(
resolve_public("127.0.0.1", 80, false).await,
Err(ResolveError::NotPublic("127.0.0.1".parse().unwrap()))
);
assert_eq!(
resolve_public("169.254.169.254", 80, false)
.await
.unwrap_err()
.to_string(),
"target address 169.254.169.254 is not public"
);
assert!(resolve_public("[::1]", 80, false).await.is_err());
assert_eq!(
resolve_public("::1", 80, false).await,
Err(ResolveError::NotPublic("::1".parse().unwrap()))
);
assert_eq!(
resolve_public("127.0.0.1", 80, true).await,
Ok("127.0.0.1:80".parse().unwrap())
);
assert_eq!(
resolve_public("no-such-host.invalid", 80, false).await,
Err(ResolveError::Resolution)
);
assert!(matches!(
resolve_public("localhost", 80, false).await,
Err(ResolveError::NotPublic(_))
));
}
#[test]
fn token_shape_and_uniqueness() {
let tokens: HashSet<String> = (0..200).map(|_| random_token(16).unwrap()).collect();
assert_eq!(tokens.len(), 200);
assert!(tokens.iter().all(|t| t.len() == 32
&& t.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())));
assert_eq!(random_token(0).unwrap(), "");
}
#[test]
fn alphanumeric_shape_and_coverage() {
let s = random_alphanumeric(2000).unwrap();
assert_eq!(s.len(), 2000);
assert!(s.bytes().all(|b| b.is_ascii_alphanumeric()));
let distinct: HashSet<u8> = s.bytes().collect();
assert_eq!(distinct.len(), 62);
assert_eq!(random_alphanumeric(0).unwrap(), "");
}
}