pinakes-server: TLS support; session persistence and security polish

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If2c9c3e3af62bbf9f33a97be89ac40bc6a6a6964
This commit is contained in:
raf 2026-01-31 15:20:27 +03:00
commit 87a4482576
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
19 changed files with 1833 additions and 111 deletions

View file

@ -3580,6 +3580,227 @@ impl StorageBackend for SqliteBackend {
.map_err(|_| PinakesError::Database("cleanup_expired_transcodes timed out".into()))?
.map_err(|e: tokio::task::JoinError| PinakesError::Database(e.to_string()))?
}
// ===== Session Management =====
async fn create_session(&self, session: &crate::storage::SessionData) -> Result<()> {
let conn = self.conn.clone();
let session_token = session.session_token.clone();
let user_id = session.user_id.clone();
let username = session.username.clone();
let role = session.role.clone();
let created_at = session.created_at.to_rfc3339();
let expires_at = session.expires_at.to_rfc3339();
let last_accessed = session.last_accessed.to_rfc3339();
let fut = tokio::task::spawn_blocking(move || {
let db = conn.lock().map_err(|e| {
PinakesError::Database(format!("failed to acquire database lock: {}", e))
})?;
db.execute(
"INSERT INTO sessions (session_token, user_id, username, role, created_at, expires_at, last_accessed)
VALUES (?, ?, ?, ?, ?, ?, ?)",
params![
&session_token,
&user_id,
&username,
&role,
&created_at,
&expires_at,
&last_accessed
],
)?;
Ok(())
});
tokio::time::timeout(std::time::Duration::from_secs(10), fut)
.await
.map_err(|_| PinakesError::Database("create_session timed out".into()))?
.map_err(|e: tokio::task::JoinError| PinakesError::Database(e.to_string()))?
}
async fn get_session(
&self,
session_token: &str,
) -> Result<Option<crate::storage::SessionData>> {
let conn = self.conn.clone();
let token = session_token.to_string();
let fut = tokio::task::spawn_blocking(move || {
let db = conn.lock().map_err(|e| {
PinakesError::Database(format!("failed to acquire database lock: {}", e))
})?;
let result = db
.query_row(
"SELECT session_token, user_id, username, role, created_at, expires_at, last_accessed
FROM sessions WHERE session_token = ?",
[&token],
|row| {
let created_at_str: String = row.get(4)?;
let expires_at_str: String = row.get(5)?;
let last_accessed_str: String = row.get(6)?;
Ok(crate::storage::SessionData {
session_token: row.get(0)?,
user_id: row.get(1)?,
username: row.get(2)?,
role: row.get(3)?,
created_at: chrono::DateTime::parse_from_rfc3339(&created_at_str)
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?
.with_timezone(&chrono::Utc),
expires_at: chrono::DateTime::parse_from_rfc3339(&expires_at_str)
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?
.with_timezone(&chrono::Utc),
last_accessed: chrono::DateTime::parse_from_rfc3339(&last_accessed_str)
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?
.with_timezone(&chrono::Utc),
})
},
)
.optional()?;
Ok(result)
});
tokio::time::timeout(std::time::Duration::from_secs(10), fut)
.await
.map_err(|_| PinakesError::Database("get_session timed out".into()))?
.map_err(|e: tokio::task::JoinError| PinakesError::Database(e.to_string()))?
}
async fn touch_session(&self, session_token: &str) -> Result<()> {
let conn = self.conn.clone();
let token = session_token.to_string();
let now = chrono::Utc::now().to_rfc3339();
let fut = tokio::task::spawn_blocking(move || {
let db = conn.lock().map_err(|e| {
PinakesError::Database(format!("failed to acquire database lock: {}", e))
})?;
db.execute(
"UPDATE sessions SET last_accessed = ? WHERE session_token = ?",
params![&now, &token],
)?;
Ok(())
});
tokio::time::timeout(std::time::Duration::from_secs(10), fut)
.await
.map_err(|_| PinakesError::Database("touch_session timed out".into()))?
.map_err(|e: tokio::task::JoinError| PinakesError::Database(e.to_string()))?
}
async fn delete_session(&self, session_token: &str) -> Result<()> {
let conn = self.conn.clone();
let token = session_token.to_string();
let fut = tokio::task::spawn_blocking(move || {
let db = conn.lock().map_err(|e| {
PinakesError::Database(format!("failed to acquire database lock: {}", e))
})?;
db.execute("DELETE FROM sessions WHERE session_token = ?", [&token])?;
Ok(())
});
tokio::time::timeout(std::time::Duration::from_secs(10), fut)
.await
.map_err(|_| PinakesError::Database("delete_session timed out".into()))?
.map_err(|e: tokio::task::JoinError| PinakesError::Database(e.to_string()))?
}
async fn delete_user_sessions(&self, username: &str) -> Result<u64> {
let conn = self.conn.clone();
let user = username.to_string();
let fut = tokio::task::spawn_blocking(move || {
let db = conn.lock().map_err(|e| {
PinakesError::Database(format!("failed to acquire database lock: {}", e))
})?;
let affected = db.execute("DELETE FROM sessions WHERE username = ?", [&user])?;
Ok(affected as u64)
});
tokio::time::timeout(std::time::Duration::from_secs(10), fut)
.await
.map_err(|_| PinakesError::Database("delete_user_sessions timed out".into()))?
.map_err(|e: tokio::task::JoinError| PinakesError::Database(e.to_string()))?
}
async fn delete_expired_sessions(&self) -> Result<u64> {
let conn = self.conn.clone();
let now = chrono::Utc::now().to_rfc3339();
let fut = tokio::task::spawn_blocking(move || {
let db = conn.lock().map_err(|e| {
PinakesError::Database(format!("failed to acquire database lock: {}", e))
})?;
let affected = db.execute("DELETE FROM sessions WHERE expires_at < ?", [&now])?;
Ok(affected as u64)
});
tokio::time::timeout(std::time::Duration::from_secs(10), fut)
.await
.map_err(|_| PinakesError::Database("delete_expired_sessions timed out".into()))?
.map_err(|e: tokio::task::JoinError| PinakesError::Database(e.to_string()))?
}
async fn list_active_sessions(
&self,
username: Option<&str>,
) -> Result<Vec<crate::storage::SessionData>> {
let conn = self.conn.clone();
let user_filter = username.map(|s| s.to_string());
let now = chrono::Utc::now().to_rfc3339();
let fut = tokio::task::spawn_blocking(move || {
let db = conn.lock().map_err(|e| {
PinakesError::Database(format!("failed to acquire database lock: {}", e))
})?;
let (query, params): (&str, Vec<String>) = if let Some(user) = user_filter {
(
"SELECT session_token, user_id, username, role, created_at, expires_at, last_accessed
FROM sessions WHERE expires_at > ? AND username = ?
ORDER BY last_accessed DESC",
vec![now, user],
)
} else {
(
"SELECT session_token, user_id, username, role, created_at, expires_at, last_accessed
FROM sessions WHERE expires_at > ?
ORDER BY last_accessed DESC",
vec![now],
)
};
let mut stmt = db.prepare(query)?;
let param_refs: Vec<&dyn rusqlite::ToSql> =
params.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
let rows = stmt.query_map(&param_refs[..], |row| {
let created_at_str: String = row.get(4)?;
let expires_at_str: String = row.get(5)?;
let last_accessed_str: String = row.get(6)?;
Ok(crate::storage::SessionData {
session_token: row.get(0)?,
user_id: row.get(1)?,
username: row.get(2)?,
role: row.get(3)?,
created_at: chrono::DateTime::parse_from_rfc3339(&created_at_str)
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?
.with_timezone(&chrono::Utc),
expires_at: chrono::DateTime::parse_from_rfc3339(&expires_at_str)
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?
.with_timezone(&chrono::Utc),
last_accessed: chrono::DateTime::parse_from_rfc3339(&last_accessed_str)
.map_err(|e| rusqlite::Error::ToSqlConversionFailure(Box::new(e)))?
.with_timezone(&chrono::Utc),
})
})?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| e.into())
});
tokio::time::timeout(std::time::Duration::from_secs(10), fut)
.await
.map_err(|_| PinakesError::Database("list_active_sessions timed out".into()))?
.map_err(|e: tokio::task::JoinError| PinakesError::Database(e.to_string()))?
}
}
// Needed for `query_row(...).optional()`