Skip to main content

soma_auth/
util.rs

1#![allow(clippy::redundant_pub_crate)]
2
3use std::fmt::Write as _;
4#[cfg(feature = "http-axum")]
5use std::net::{IpAddr, SocketAddr};
6use std::path::Path;
7#[cfg(feature = "http-axum")]
8use std::time::Duration;
9
10#[cfg(feature = "http-axum")]
11use base64::Engine;
12#[cfg(feature = "http-axum")]
13use base64::engine::general_purpose::URL_SAFE_NO_PAD;
14use sha2::{Digest, Sha256};
15
16use crate::error::AuthError;
17
18/// Extract the `IpAddr` from a `SocketAddr`, normalizing IPv4-mapped IPv6
19/// addresses (`::ffff:a.b.c.d`) back to plain IPv4 so per-IP rate-limiting
20/// keys are consistent regardless of listener address family.
21#[cfg(feature = "http-axum")]
22pub(crate) fn remote_ip(addr: SocketAddr) -> IpAddr {
23    match addr.ip() {
24        IpAddr::V6(v6) => v6
25            .to_ipv4_mapped()
26            .map(IpAddr::V4)
27            .unwrap_or(IpAddr::V6(v6)),
28        v4 => v4,
29    }
30}
31
32pub fn now_unix() -> i64 {
33    let secs = std::time::SystemTime::now()
34        .duration_since(std::time::UNIX_EPOCH)
35        .unwrap_or_default()
36        .as_secs();
37    i64::try_from(secs).unwrap_or(i64::MAX)
38}
39
40#[cfg(feature = "http-axum")]
41pub(crate) fn random_token(bytes: usize) -> Result<String, AuthError> {
42    let mut buf = vec![0_u8; bytes];
43    getrandom::fill(&mut buf)
44        .map_err(|error| AuthError::Storage(format!("generate random token: {error}")))?;
45    Ok(URL_SAFE_NO_PAD.encode(buf))
46}
47
48pub fn fingerprint(value: &str) -> String {
49    let digest = Sha256::digest(value.as_bytes());
50    let mut output = String::with_capacity(12);
51    for byte in &digest[..6] {
52        let _ = write!(&mut output, "{byte:02x}");
53    }
54    output
55}
56
57#[cfg(unix)]
58pub(crate) fn ensure_restrictive_permissions(path: &Path) -> Result<(), AuthError> {
59    use std::os::unix::fs::PermissionsExt;
60
61    let metadata = std::fs::metadata(path)
62        .map_err(|error| AuthError::Storage(format!("stat `{}`: {error}", path.display())))?;
63    let mode = metadata.permissions().mode() & 0o777;
64    if mode & 0o077 != 0 {
65        return Err(AuthError::InsecurePermissions {
66            path: path.to_path_buf(),
67        });
68    }
69    Ok(())
70}
71
72#[cfg(not(unix))]
73pub(crate) fn ensure_restrictive_permissions(_path: &Path) -> Result<(), AuthError> {
74    Ok(())
75}
76
77#[cfg(unix)]
78pub(crate) fn set_restrictive_permissions(path: &Path) -> Result<(), AuthError> {
79    use std::os::unix::fs::PermissionsExt;
80
81    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
82        .map_err(|error| AuthError::Storage(format!("chmod 0600 `{}`: {error}", path.display())))
83}
84
85#[cfg(not(unix))]
86pub(crate) fn set_restrictive_permissions(_path: &Path) -> Result<(), AuthError> {
87    Ok(())
88}
89
90#[cfg(feature = "http-axum")]
91pub(crate) fn duration_secs_i64(duration: Duration, field: &str) -> Result<i64, AuthError> {
92    i64::try_from(duration.as_secs())
93        .map_err(|_| AuthError::Config(format!("{field} exceeds supported range")))
94}
95
96#[cfg(feature = "http-axum")]
97pub(crate) fn duration_secs_usize(duration: Duration, field: &str) -> Result<usize, AuthError> {
98    usize::try_from(duration.as_secs())
99        .map_err(|_| AuthError::Config(format!("{field} exceeds supported range")))
100}
101
102#[cfg(feature = "http-axum")]
103pub(crate) fn timestamp_usize(timestamp: i64, field: &str) -> Result<usize, AuthError> {
104    usize::try_from(timestamp)
105        .map_err(|_| AuthError::Storage(format!("{field} is negative or exceeds usize range")))
106}
107
108#[cfg(feature = "http-axum")]
109pub(crate) fn expires_at(
110    created_at: i64,
111    duration: Duration,
112    field: &str,
113) -> Result<i64, AuthError> {
114    let ttl = duration_secs_i64(duration, field)?;
115    created_at
116        .checked_add(ttl)
117        .ok_or_else(|| AuthError::Config(format!("{field} exceeds supported range")))
118}