Files
gamestream-webtransport-proxy/gamestream-webtransport-proxy/src/backend.rs
T
restitux 22f9405229 backend: add user management system with SQLite database
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>
2026-04-16 02:34:02 +00:00

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,
})
}
}