use serde::{Deserialize, Serialize};
use crate::storage::errors::StorageError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheData {
pub value: String,
}
#[derive(Debug, Clone)]
pub struct CachePrefix(String);
impl CachePrefix {
pub fn new(prefix: String) -> Result<Self, StorageError> {
if prefix.is_empty() {
tracing::debug!("Empty cache prefix component");
}
if prefix.len() > 250 {
return Err(StorageError::InvalidInput(format!(
"Cache prefix component too long: {} bytes (max 250)",
prefix.len()
)));
}
let dangerous_chars = ['\n', '\r', ' ', '\t'];
if prefix.chars().any(|c| dangerous_chars.contains(&c)) {
return Err(StorageError::InvalidInput(format!(
"Cache prefix component contains unsafe characters (whitespace/newlines): '{prefix}'"
)));
}
let prefix_upper = prefix.to_uppercase();
let redis_commands = [
"SET", "GET", "DEL", "FLUSHDB", "FLUSHALL", "EVAL", "SCRIPT", "SHUTDOWN", "CONFIG",
"CLIENT", "DEBUG", "MONITOR", "SYNC",
];
for cmd in &redis_commands {
if prefix_upper == *cmd
|| prefix_upper.starts_with(&format!("{cmd} "))
|| prefix_upper.ends_with(&format!(" {cmd}"))
|| prefix_upper.contains(&format!(" {cmd} "))
|| prefix_upper.starts_with(&format!("{cmd}\n"))
|| prefix_upper.ends_with(&format!("\n{cmd}"))
|| prefix_upper.contains(&format!("\n{cmd}\n"))
{
return Err(StorageError::InvalidInput(format!(
"Cache prefix component contains potentially dangerous command keyword: '{prefix}'"
)));
}
}
Ok(CachePrefix(prefix))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn session() -> Self {
CachePrefix("session".to_string())
}
pub fn aaguid() -> Self {
CachePrefix("aaguid".to_string())
}
#[allow(dead_code)]
pub fn challenge() -> Self {
CachePrefix("challenge".to_string())
}
pub fn pkce() -> Self {
CachePrefix("pkce".to_string())
}
pub fn nonce() -> Self {
CachePrefix("nonce".to_string())
}
pub fn csrf() -> Self {
CachePrefix("csrf".to_string())
}
pub fn misc_session() -> Self {
CachePrefix("misc_session".to_string())
}
pub fn mode() -> Self {
CachePrefix("mode".to_string())
}
pub fn jwks() -> Self {
CachePrefix("jwks".to_string())
}
pub fn auth_challenge() -> Self {
CachePrefix("authentication".to_string())
}
pub fn reg_challenge() -> Self {
CachePrefix("registration".to_string())
}
pub fn session_info() -> Self {
CachePrefix("session_info".to_string())
}
pub fn user_sessions() -> Self {
CachePrefix("user_sessions".to_string())
}
}
#[derive(Debug, Clone)]
pub struct CacheKey(String);
impl CacheKey {
pub fn new(key: String) -> Result<Self, StorageError> {
if key.is_empty() {
tracing::debug!("Empty cache key component");
}
if key.len() > 250 {
return Err(StorageError::InvalidInput(format!(
"Cache key component too long: {} bytes (max 250)",
key.len()
)));
}
let dangerous_chars = ['\n', '\r', ' ', '\t'];
if key.chars().any(|c| dangerous_chars.contains(&c)) {
return Err(StorageError::InvalidInput(format!(
"Cache key component contains unsafe characters (whitespace/newlines): '{key}'"
)));
}
let key_upper = key.to_uppercase();
let redis_commands = [
"SET", "GET", "DEL", "FLUSHDB", "FLUSHALL", "EVAL", "SCRIPT", "SHUTDOWN", "CONFIG",
"CLIENT", "DEBUG", "MONITOR", "SYNC",
];
for cmd in &redis_commands {
if key_upper == *cmd
|| key_upper.starts_with(&format!("{cmd} "))
|| key_upper.ends_with(&format!(" {cmd}"))
|| key_upper.contains(&format!(" {cmd} "))
|| key_upper.starts_with(&format!("{cmd}\n"))
|| key_upper.ends_with(&format!("\n{cmd}"))
|| key_upper.contains(&format!("\n{cmd}\n"))
{
return Err(StorageError::InvalidInput(format!(
"Cache key component contains potentially dangerous command keyword: '{key}'"
)));
}
}
Ok(CacheKey(key))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[cfg(test)]
mod tests;