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 axum::Json;
12#[cfg(feature = "http-axum")]
13use axum::http::{HeaderValue, StatusCode, header};
14#[cfg(feature = "http-axum")]
15use axum::response::{IntoResponse, Response};
16#[cfg(feature = "http-axum")]
17use base64::Engine;
18#[cfg(feature = "http-axum")]
19use base64::engine::general_purpose::URL_SAFE_NO_PAD;
20use sha2::{Digest, Sha256};
21
22use crate::error::AuthError;
23#[cfg(feature = "http-axum")]
24use crate::error::AuthErrorKind;
25
26#[cfg(feature = "http-axum")]
30pub(crate) fn remote_ip(addr: SocketAddr) -> IpAddr {
31 match addr.ip() {
32 IpAddr::V6(v6) => v6
33 .to_ipv4_mapped()
34 .map(IpAddr::V4)
35 .unwrap_or(IpAddr::V6(v6)),
36 v4 => v4,
37 }
38}
39
40pub fn now_unix() -> i64 {
41 let secs = std::time::SystemTime::now()
42 .duration_since(std::time::UNIX_EPOCH)
43 .unwrap_or_default()
44 .as_secs();
45 i64::try_from(secs).unwrap_or(i64::MAX)
46}
47
48#[cfg(feature = "http-axum")]
49pub(crate) fn random_token(bytes: usize) -> Result<String, AuthError> {
50 let mut buf = vec![0_u8; bytes];
51 getrandom::fill(&mut buf)
52 .map_err(|error| AuthError::Storage(format!("generate random token: {error}")))?;
53 Ok(URL_SAFE_NO_PAD.encode(buf))
54}
55
56pub fn fingerprint(value: &str) -> String {
57 let digest = Sha256::digest(value.as_bytes());
58 let mut output = String::with_capacity(12);
59 for byte in &digest[..6] {
60 let _ = write!(&mut output, "{byte:02x}");
61 }
62 output
63}
64
65#[cfg(unix)]
66pub(crate) fn ensure_restrictive_permissions(path: &Path) -> Result<(), AuthError> {
67 use std::os::unix::fs::PermissionsExt;
68
69 let metadata = std::fs::metadata(path)
70 .map_err(|error| AuthError::Storage(format!("stat `{}`: {error}", path.display())))?;
71 let mode = metadata.permissions().mode() & 0o777;
72 if mode & 0o077 != 0 {
73 return Err(AuthError::InsecurePermissions {
74 path: path.to_path_buf(),
75 });
76 }
77 Ok(())
78}
79
80#[cfg(not(unix))]
81pub(crate) fn ensure_restrictive_permissions(_path: &Path) -> Result<(), AuthError> {
82 Ok(())
83}
84
85#[cfg(unix)]
86pub(crate) fn set_restrictive_permissions(path: &Path) -> Result<(), AuthError> {
87 use std::os::unix::fs::PermissionsExt;
88
89 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
90 .map_err(|error| AuthError::Storage(format!("chmod 0600 `{}`: {error}", path.display())))
91}
92
93#[cfg(not(unix))]
94pub(crate) fn set_restrictive_permissions(_path: &Path) -> Result<(), AuthError> {
95 Ok(())
96}
97
98#[cfg(feature = "http-axum")]
99pub(crate) fn duration_secs_i64(duration: Duration, field: &str) -> Result<i64, AuthError> {
100 i64::try_from(duration.as_secs())
101 .map_err(|_| AuthError::Config(format!("{field} exceeds supported range")))
102}
103
104#[cfg(feature = "http-axum")]
105pub(crate) fn duration_secs_usize(duration: Duration, field: &str) -> Result<usize, AuthError> {
106 usize::try_from(duration.as_secs())
107 .map_err(|_| AuthError::Config(format!("{field} exceeds supported range")))
108}
109
110#[cfg(feature = "http-axum")]
111pub(crate) fn timestamp_usize(timestamp: i64, field: &str) -> Result<usize, AuthError> {
112 usize::try_from(timestamp)
113 .map_err(|_| AuthError::Storage(format!("{field} is negative or exceeds usize range")))
114}
115
116#[cfg(feature = "http-axum")]
126pub(crate) fn apply_cache_control_no_store(mut response: Response) -> Response {
127 response
128 .headers_mut()
129 .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
130 response
131}
132
133#[cfg(feature = "http-axum")]
140pub(crate) fn apply_no_store(response: Response) -> Response {
141 let mut response = apply_cache_control_no_store(response);
142 response
143 .headers_mut()
144 .insert(header::PRAGMA, HeaderValue::from_static("no-cache"));
145 response
146}
147
148#[cfg(feature = "http-axum")]
159pub(crate) fn oauth_error_response(
160 status: StatusCode,
161 oauth_error: &'static str,
162 description: String,
163 log_kind: &'static str,
164 retry_after_ms: Option<u64>,
165) -> Response {
166 let body = Json(serde_json::json!({
167 "error": oauth_error,
168 "error_description": description,
169 }));
170 let mut response = (status, body).into_response();
171 response.extensions_mut().insert(AuthErrorKind(log_kind));
172 if let Some(retry_after_ms) = retry_after_ms
173 && let Ok(value) = HeaderValue::from_str(&(retry_after_ms / 1_000).max(1).to_string())
174 {
175 response.headers_mut().insert(header::RETRY_AFTER, value);
176 }
177 apply_no_store(response)
178}
179
180#[cfg(feature = "http-axum")]
181pub(crate) fn expires_at(
182 created_at: i64,
183 duration: Duration,
184 field: &str,
185) -> Result<i64, AuthError> {
186 let ttl = duration_secs_i64(duration, field)?;
187 created_at
188 .checked_add(ttl)
189 .ok_or_else(|| AuthError::Config(format!("{field} exceeds supported range")))
190}