22f9405229
Add authentication and authorization infrastructure: - SQLite database (db.rs) with users, sessions, and app permissions tables - Password hashing with argon2 - Session-based auth with random 256-bit tokens - Auth middleware (session validation) and admin middleware - Login/logout/me endpoints - Admin CRUD endpoints for user and permission management - Auto-seed default admin user on first run - 23 unit tests covering all DB operations Existing API endpoints are not yet gated behind auth. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
115 lines
3.1 KiB
Rust
115 lines
3.1 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use anyhow::Result;
|
|
use salvo::oapi::ToSchema;
|
|
use serde::{Deserialize, Serialize};
|
|
use tokio::sync::RwLock;
|
|
|
|
use crate::db::Db;
|
|
use crate::state::StateFile;
|
|
|
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
|
pub struct InputCrypto {
|
|
pub aes_key: [u8; 16],
|
|
pub aes_iv: [u8; 16],
|
|
}
|
|
|
|
impl InputCrypto {
|
|
pub fn new() -> Result<Self> {
|
|
let mut aes_key = [0u8; 16];
|
|
let mut aes_iv = [0u8; 16];
|
|
openssl::rand::rand_bytes(&mut aes_key)?;
|
|
openssl::rand::rand_bytes(&mut aes_iv)?;
|
|
|
|
Ok(InputCrypto { aes_key, aes_iv })
|
|
}
|
|
|
|
pub fn as_url_params(&self) -> (String, String) {
|
|
let aes_key_hex = hex::encode(self.aes_key);
|
|
|
|
// not sure why we have to do this but I'm just matching the embedded client behavior
|
|
// 1. Generate 16 random bytes
|
|
// 2. memcpy the first 4 bytes to a u32 (from_le_bytes)
|
|
// 3. Convert that u32 to network order (to_be) and convert it to a string with %d
|
|
// (to_string)
|
|
let aes_iv_array = <[u8; 4]>::try_from(&self.aes_iv[0..4]).unwrap();
|
|
let aes_iv_u32 = u32::from_le_bytes(aes_iv_array);
|
|
let aes_iv_string = aes_iv_u32.to_be().to_string();
|
|
(aes_key_hex, aes_iv_string)
|
|
}
|
|
|
|
pub fn as_stream_config_params(&self) -> ([i8; 16], [i8; 16]) {
|
|
let aes_key_ptr = &self.aes_key as *const [u8; 16];
|
|
let aes_key_ptr_i8 = aes_key_ptr as *const [i8; 16];
|
|
let aes_key_i8 = unsafe { *aes_key_ptr_i8 };
|
|
|
|
let aes_iv_ptr = &self.aes_iv as *const [u8; 16];
|
|
let aes_iv_ptr_i8 = aes_iv_ptr as *const [i8; 16];
|
|
let aes_iv_i8 = unsafe { *aes_iv_ptr_i8 };
|
|
|
|
(aes_key_i8, aes_iv_i8)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, ToSchema, Serialize, Deserialize)]
|
|
pub struct Mode {
|
|
pub width: i32,
|
|
pub height: i32,
|
|
pub fps: i32,
|
|
}
|
|
|
|
impl Mode {
|
|
pub fn as_url_string(&self) -> String {
|
|
format!("{}x{}x{}", self.width, self.height, self.fps)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, ToSchema, Serialize, Deserialize)]
|
|
pub struct StreamConfig {
|
|
pub mode: Mode,
|
|
pub bitrate_kbps: i32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Stream {
|
|
pub id: uuid::Uuid,
|
|
|
|
pub url: url::Url,
|
|
pub game_session: u64,
|
|
|
|
pub server_address: String,
|
|
pub input_crypto: InputCrypto,
|
|
pub stream_config: StreamConfig,
|
|
|
|
pub app_version: String,
|
|
pub gfe_version: String,
|
|
pub server_codec_mode_support: i32,
|
|
}
|
|
|
|
pub struct Backend {
|
|
pub state: StateFile,
|
|
pub streams: RwLock<HashMap<uuid::Uuid, Stream>>,
|
|
pub port: u16,
|
|
pub db: Db,
|
|
}
|
|
|
|
impl Backend {
|
|
pub fn new(port: u16) -> Result<Self> {
|
|
let project_dirs =
|
|
directories::ProjectDirs::from("xyz", "ohea", "gamestream-webtransport-proxy")
|
|
.ok_or(anyhow::anyhow!("Could not get project dirs"))?;
|
|
let data_dir = project_dirs.data_dir();
|
|
std::fs::create_dir_all(data_dir)?;
|
|
let db_path = data_dir.join("auth.db");
|
|
|
|
let db = Db::open(&db_path)?;
|
|
|
|
Ok(Backend {
|
|
state: StateFile::new()?,
|
|
streams: RwLock::new(HashMap::new()),
|
|
port,
|
|
db,
|
|
})
|
|
}
|
|
}
|