This commit is contained in:
2026-06-22 15:19:46 +01:00
parent de96bc030d
commit 808d6ac5db
21 changed files with 948 additions and 282 deletions
+179 -40
View File
@@ -1,30 +1,30 @@
use std::{
hash::{DefaultHasher, Hash, Hasher},
net::SocketAddr,
sync::Arc,
};
use std::{net::SocketAddr, sync::Arc};
use axum::{
Json, Router,
body::Bytes,
extract::{Form, Query, State},
http::StatusCode,
body::{Body, Bytes},
extract::{Form, FromRequestParts, Path, Query, State},
http::{HeaderMap, StatusCode, header::SET_COOKIE, request::Parts},
response::{IntoResponse, Response},
routing::{get, post},
};
use entity::{auth_session, stream_key, users};
use sea_orm::DatabaseConnection;
use serde::{Deserialize, Serialize};
use tokio::{
net::TcpListener,
sync::{Mutex, mpsc::Sender},
};
use uuid::Uuid;
use crate::AppState;
use crate::{AppState, hash::hash_password};
pub struct HttpServer {
pub offer_tx: Sender<(i32, String, String)>,
pub accept_rx: async_broadcast::Receiver<(i32, String)>,
pub offer_tx: Sender<(i32, i32, String)>,
pub accept_rx: async_broadcast::Receiver<(i32, Option<String>)>,
pub appstate: Arc<Mutex<AppState>>,
pub request_count: Mutex<i32>,
pub db: DatabaseConnection,
}
impl HttpServer {
@@ -35,7 +35,12 @@ impl HttpServer {
let app = Router::new()
.route("/api/catalog", get(catalog_handler))
.route("/api/user", post(create_user_handler))
.route("/api/stream", get(stream_handler))
.route(
"/api/stream-key",
post(create_stream_key_handler).get(get_all_stream_keys),
)
.route("/api/login", post(login_handler))
.route("/api/stream/{slug}", post(stream_handler))
.route("/api/meow", get(meow_handler))
.with_state(state);
@@ -47,17 +52,103 @@ impl HttpServer {
}
}
fn calculate_hash<T: Hash>(t: &T) -> u64 {
let mut s = DefaultHasher::new();
t.hash(&mut s);
s.finish()
}
async fn catalog_handler(State(state): State<Arc<HttpServer>>) -> impl IntoResponse {
let catalog = catalog_from_state(&state.appstate).await;
Json(catalog)
}
#[derive(Deserialize)]
struct CreateStreamKeyBody {
label: String,
}
struct AuthUser(entity::users::Model);
impl FromRequestParts<Arc<HttpServer>> for AuthUser {
type Rejection = StatusCode;
async fn from_request_parts(
parts: &mut Parts,
state: &Arc<HttpServer>,
) -> Result<Self, StatusCode> {
let token = parts
.headers
.get("session")
// parse token...
.ok_or(StatusCode::UNAUTHORIZED)?;
let user =
users::Entity::find_by_auth_session(&state.db, token.to_str().unwrap().to_string())
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::UNAUTHORIZED)?;
Ok(AuthUser(user))
}
}
async fn create_stream_key_handler(
State(state): State<Arc<HttpServer>>,
auth: AuthUser,
Json(payload): Json<CreateStreamKeyBody>,
) -> impl IntoResponse {
let uuid = uuid::Uuid::new_v4();
let value = format!("stream-key-{uuid}");
let key = stream_key::Entity::create(&state.db, auth.0.id, value, payload.label, false).await;
if let Ok(_key) = key {
StatusCode::CREATED
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
}
struct StreamKeys {
keys: Vec<StreamKey>,
}
struct StreamKey {
id: i32,
label: String,
value: String,
}
async fn get_all_stream_keys(
State(state): State<Arc<HttpServer>>,
auth: AuthUser,
) -> impl IntoResponse {
let user_keys = stream_key::Entity::find_by_user(&state.db, auth.0.id)
.await
.unwrap();
match stream_key::Entity::find_by_user(&state.db, auth.0.id).await {
Ok(keys) => Json(keys).into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
}
}
#[derive(Deserialize)]
struct LoginForm {
username: String,
password: String,
}
#[derive(Serialize)]
struct LoginResponse {
session_token: String,
}
async fn login_handler(
State(state): State<Arc<HttpServer>>,
Json(payload): Json<LoginForm>,
) -> StatusCode {
// TODO: hash payload.password with a real password hasher (argon2/bcrypt), look up user by username
// TODO: verify hash matches stored hashed_password
// TODO: call auth_session::Entity::create, return session token
// TODO: return 401 on bad credentials
todo!("login not yet implemented")
}
#[derive(Deserialize)]
struct CreateUserForm {
username: String,
@@ -65,44 +156,91 @@ struct CreateUserForm {
ref_token: String,
}
async fn create_user_handler(Form(form): Form<CreateUserForm>) -> StatusCode {
// TODO: ref_token validation, db integration
let _ = (
form.username,
calculate_hash(&form.password),
form.ref_token,
);
todo!("user creation not yet implemented")
}
async fn create_user_handler(
State(state): State<Arc<HttpServer>>,
Json(payload): Json<CreateUserForm>,
) -> (HeaderMap, StatusCode) {
if payload.ref_token != "TEST" {
return (HeaderMap::new(), StatusCode::UNAUTHORIZED);
}
#[derive(Deserialize)]
struct StreamQuery {
stream_label: String,
let meow = users::Entity::create(
&state.db,
payload.username,
hash_password(&payload.password).unwrap(),
)
.await;
// Create session
let session: auth_session::Model = if let Ok(meow) = meow {
auth_session::Entity::create(&state.db, meow.id)
.await
.unwrap() // This should be ok (hopefully)
} else {
return (HeaderMap::new(), StatusCode::CONFLICT);
};
let token = session.value;
let mut headers = HeaderMap::new();
headers.insert(
SET_COOKIE,
format!("session={token}; HttpOnly; SameSite=Strict; Path=/")
.parse()
.unwrap(),
);
(headers, StatusCode::OK)
}
async fn stream_handler(
State(state): State<Arc<HttpServer>>,
Query(query): Query<StreamQuery>,
body: Bytes,
Path(slug): Path<String>,
body: String,
) -> impl IntoResponse {
let body_str = String::from_utf8(body.to_vec()).unwrap_or_default();
let mut request_id = state.request_count.lock().await;
*request_id += 1;
let request_id_clone = *request_id;
drop(request_id);
let stream_key_id = {
let app = state.appstate.lock().await;
app.stream_sessions
.iter()
.find(|e| e.value().stream_key_label == slug)
.map(|e| *e.key())
};
let stream_key_id = if let Some(id) = stream_key_id {
id
} else {
return Response::builder()
.status(StatusCode::NOT_FOUND)
.header("content-type", "application/text")
.body("".to_string())
.unwrap();
};
let mut accept_rx = state.accept_rx.new_receiver();
let _ = state
.offer_tx
.send((request_id_clone, query.stream_label, body_str))
.await;
.send((request_id_clone, stream_key_id, body))
.await
.unwrap();
let mut reply_body = String::new();
while let Ok(answer) = accept_rx.recv().await {
if answer.0 == request_id_clone {
reply_body = answer.1;
break;
if let Some(reply) = answer.1 {
if answer.0 == request_id_clone {
Response::builder()
.status(StatusCode::CREATED)
.header("content-type", "application/sdp")
.body(reply)
.unwrap();
}
} else {
Response::builder()
.status(StatusCode::NOT_FOUND)
// .header("content-type", "application/sdp")
.body("")
.unwrap();
}
}
@@ -119,9 +257,10 @@ async fn meow_handler() -> &'static str {
#[derive(Serialize)]
struct StreamCatalog {
active_streams: Vec<String>,
active_streams: Vec<i32>,
}
// TODO: Make this return streak labels and their id instead of the whole stream key omfg
async fn catalog_from_state(state: &Arc<Mutex<AppState>>) -> StreamCatalog {
let app = state.lock().await;
let active_streams = app