diff --git a/src/main.rs b/src/main.rs index 98819ee..04f0197 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,19 @@ mod proto; mod entities; +mod routes; use std::{collections::HashMap, sync::Arc}; -use chrono::{DateTime, Utc, Duration}; +use chrono::{DateTime, Utc}; use clap::Parser; -use axum::{Router, routing::{get, post}, response::IntoResponse, Json, http::StatusCode, extract::{State, Query}}; -use proto::{Error, AuthenticateRequest, AuthenticateResponse, JoinRequest, JoinResponse, ValidateRequest, RefreshRequest, RefreshResponse, RegisterRequest}; -use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, DatabaseConnection, Database, ActiveValue::NotSet, Set}; +use axum::{Router, routing::{get, post}, response::IntoResponse, Json, http::StatusCode, extract::State}; +use sea_orm::{EntityTrait, DatabaseConnection, Database, ActiveValue::NotSet, Set}; use tokio::sync::Mutex; use uuid::Uuid; -use tracing::{info, warn}; +use tracing::info; -use crate::proto::Property; +use crate::routes::auth::{authenticate, validate, refresh}; +use crate::routes::session::{join, has_joined}; /// Reimplementation of legacy auth server for minecraft #[derive(Parser, Debug, Clone)] @@ -31,7 +32,7 @@ struct ConfigArgs { } #[derive(Clone)] -struct JoinAttempt { +pub struct JoinAttempt { server: String, time: DateTime, } @@ -43,7 +44,7 @@ impl JoinAttempt { } #[derive(Clone)] -struct AppState { +pub struct AppState { store: Arc>>, db: DatabaseConnection, cfg: ConfigArgs, @@ -82,7 +83,7 @@ async fn main() -> Result<(), Box> { } async fn fallback_route() -> impl IntoResponse { - let error = Error { + let error = proto::Error { error: "No valid route specified".into(), errorMessage: "No route requested or invalid route specified".into(), cause: "Routing".into(), @@ -91,157 +92,7 @@ async fn fallback_route() -> impl IntoResponse { (StatusCode::OK, Json(error)) } -async fn validate(State(state): State, Json(payload): Json) -> StatusCode { - let token = entities::token::Entity::find().filter( - entities::token::Column::AccessToken.eq(payload.accessToken) - ).one(&state.db).await.unwrap(); - - if let Some(_t) = token { - StatusCode::NO_CONTENT - } else { - StatusCode::UNAUTHORIZED - } -} - -async fn refresh(State(state): State, Json(payload): Json) -> Result, StatusCode> { - let token = entities::token::Entity::find().filter( - entities::token::Column::AccessToken.eq(payload.accessToken.clone()) - ).one(&state.db).await.unwrap(); - - if let Some(t) = token { - // TODO if user requests profile, fetch it and include it - let user = entities::user::Entity::find_by_id(t.user_id).one(&state.db).await.unwrap().unwrap(); - entities::token::Entity::delete( - entities::token::ActiveModel{ - id: NotSet, - access_token: Set(payload.accessToken.clone()), - created_at: NotSet, - user_id: NotSet, - } - ).exec(&state.db).await.unwrap(); - let new_access_token = Uuid::new_v4(); // TODO same as with authenticate - entities::token::Entity::insert( - entities::token::ActiveModel{ - id: NotSet, - access_token: Set(payload.accessToken.clone()), - created_at: Set(Utc::now()), - user_id: Set(user.id), - } - ).exec(&state.db).await.unwrap(); - Ok(Json( - RefreshResponse { - accessToken: new_access_token.to_string(), - clientToken: "idc".into(), - selectedProfile: proto::Profile { id: user.uuid, name: user.name }, - user: None, - } - )) - } else { - Err(StatusCode::UNAUTHORIZED) - } -} - -async fn authenticate(State(state): State, Json(payload): Json) -> Result, StatusCode> { - let user = entities::user::Entity::find().filter( - entities::user::Column::Name.eq(payload.username) - ).one(&state.db).await.unwrap(); - - if let Some(u) = user { - if payload.password == u.password { - // make new token - let access_token = Uuid::new_v4().to_string(); // TODO maybe use a JWT? - entities::token::Entity::insert(entities::token::ActiveModel { - id: NotSet, - user_id: Set(u.id), - access_token: Set(access_token.clone()), - created_at: Set(Utc::now()), - }).exec(&state.db).await.unwrap(); - let profile = proto::Profile { - name: u.name.clone(), - id: u.uuid, - }; - Ok(Json( - AuthenticateResponse { - accessToken: access_token, - user: proto::User { id: u.uuid, username: u.name, properties: Some(vec![]) }, - clientToken: Uuid::new_v4().to_string(), - availableProfiles: vec![profile.clone()], - selectedProfile: profile, - } - )) - } else { - Err(StatusCode::UNAUTHORIZED) - } - } else { - Err(StatusCode::NOT_FOUND) - } -} - -async fn join(State(state): State, Json(payload): Json) -> StatusCode { - let user = entities::user::Entity::find().filter( - entities::user::Column::Uuid.eq(payload.selectedProfile.id) - ).one(&state.db).await.unwrap().unwrap(); - - let tokens = entities::token::Entity::find().filter( - entities::token::Column::UserId.eq(user.id) - ).all(&state.db).await.unwrap(); - - if tokens.iter().any(|x| x.access_token == payload.accessToken) { - state.store.lock().await.insert(payload.selectedProfile.id, JoinAttempt::new(payload.serverId.clone())); - info!(target: "JOIN", "user {} has joined server {}", payload.selectedProfile.name, payload.serverId); - StatusCode::OK - } else { - warn!(target: "JOIN", "user {} attempted to join server {} without a valid token", payload.selectedProfile.name, payload.serverId); - StatusCode::UNAUTHORIZED - } -} - -async fn has_joined(State(state): State, Query(query): Query>) -> Result, StatusCode> { - let username = query.get("username").unwrap().clone(); - let server_id = query.get("serverId").unwrap(); - - let user = entities::user::Entity::find().filter( - entities::user::Column::Name.eq(username.clone()) - ).one(&state.db).await.unwrap(); - - match user { - Some(user) => { - match state.store.lock().await.get(&user.uuid) { - Some(join) => { - if Utc::now() - join.time < Duration::seconds(state.cfg.time_window as i64) - && join.server.to_lowercase() == server_id.to_lowercase() { - let response = JoinResponse { - id: user.uuid, - name: username.clone(), - properties: vec![ - Property { - name: "textures".into(), - value: "".into(), - signature: Some("".into()), - } - ], - }; - info!(target: "hasJOINED", "server found user -> {:?}", response); - Ok(Json(response)) - } else { - warn!(target: "hasJOINED", "server found user but join was late or for another server"); - Err(StatusCode::NOT_FOUND) - } - }, - None => { - warn!(target: "hasJOINED", "server didn't find user"); - Err(StatusCode::NOT_FOUND) - }, - } - }, - None => { - warn!(target: "hasJOINED", "invalid UUID"); - Err(StatusCode::NOT_FOUND) - }, - } -} - -async fn register_tmp(State(state): State, Json(payload): Json) -> StatusCode { +async fn register_tmp(State(state): State, Json(payload): Json) -> StatusCode { let user = entities::user::ActiveModel { id: NotSet, name: Set(payload.user), diff --git a/src/routes/auth.rs b/src/routes/auth.rs new file mode 100644 index 0000000..e81646c --- /dev/null +++ b/src/routes/auth.rs @@ -0,0 +1,93 @@ +use axum::{extract::State, Json, http::StatusCode}; +use chrono::Utc; +use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, ActiveValue::NotSet, Set}; +use uuid::Uuid; + +use crate::{entities, AppState, proto}; + + +pub async fn validate(State(state): State, Json(payload): Json) -> StatusCode { + let token = entities::token::Entity::find().filter( + entities::token::Column::AccessToken.eq(payload.accessToken) + ).one(&state.db).await.unwrap(); + + if let Some(_t) = token { + StatusCode::NO_CONTENT + } else { + StatusCode::UNAUTHORIZED + } +} + +pub async fn refresh(State(state): State, Json(payload): Json) -> Result, StatusCode> { + let token = entities::token::Entity::find().filter( + entities::token::Column::AccessToken.eq(payload.accessToken.clone()) + ).one(&state.db).await.unwrap(); + + if let Some(t) = token { + // TODO if user requests profile, fetch it and include it + let user = entities::user::Entity::find_by_id(t.user_id).one(&state.db).await.unwrap().unwrap(); + entities::token::Entity::delete( + entities::token::ActiveModel{ + id: NotSet, + access_token: Set(payload.accessToken.clone()), + created_at: NotSet, + user_id: NotSet, + } + ).exec(&state.db).await.unwrap(); + let new_access_token = Uuid::new_v4(); // TODO same as with authenticate + entities::token::Entity::insert( + entities::token::ActiveModel{ + id: NotSet, + access_token: Set(payload.accessToken.clone()), + created_at: Set(Utc::now()), + user_id: Set(user.id), + } + ).exec(&state.db).await.unwrap(); + Ok(Json( + proto::RefreshResponse { + accessToken: new_access_token.to_string(), + clientToken: "idc".into(), + selectedProfile: proto::Profile { id: user.uuid, name: user.name }, + user: None, + } + )) + } else { + Err(StatusCode::UNAUTHORIZED) + } +} + +pub async fn authenticate(State(state): State, Json(payload): Json) -> Result, StatusCode> { + let user = entities::user::Entity::find().filter( + entities::user::Column::Name.eq(payload.username) + ).one(&state.db).await.unwrap(); + + if let Some(u) = user { + if payload.password == u.password { + // make new token + let access_token = Uuid::new_v4().to_string(); // TODO maybe use a JWT? + entities::token::Entity::insert(entities::token::ActiveModel { + id: NotSet, + user_id: Set(u.id), + access_token: Set(access_token.clone()), + created_at: Set(Utc::now()), + }).exec(&state.db).await.unwrap(); + let profile = proto::Profile { + name: u.name.clone(), + id: u.uuid, + }; + Ok(Json( + proto::AuthenticateResponse { + accessToken: access_token, + user: proto::User { id: u.uuid, username: u.name, properties: Some(vec![]) }, + clientToken: Uuid::new_v4().to_string(), + availableProfiles: vec![profile.clone()], + selectedProfile: profile, + } + )) + } else { + Err(StatusCode::UNAUTHORIZED) + } + } else { + Err(StatusCode::NOT_FOUND) + } +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..f373eb7 --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,2 @@ +pub mod auth; +pub mod session; diff --git a/src/routes/session.rs b/src/routes/session.rs new file mode 100644 index 0000000..c55d6c7 --- /dev/null +++ b/src/routes/session.rs @@ -0,0 +1,73 @@ +use std::collections::HashMap; + +use axum::{extract::{State, Query}, http::StatusCode, Json}; +use chrono::{Duration, Utc}; +use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; +use tracing::{info, warn}; + +use crate::{AppState, proto, JoinAttempt, entities}; + + +pub async fn join(State(state): State, Json(payload): Json) -> StatusCode { + let user = entities::user::Entity::find().filter( + entities::user::Column::Uuid.eq(payload.selectedProfile.id) + ).one(&state.db).await.unwrap().unwrap(); + + let tokens = entities::token::Entity::find().filter( + entities::token::Column::UserId.eq(user.id) + ).all(&state.db).await.unwrap(); + + if tokens.iter().any(|x| x.access_token == payload.accessToken) { + state.store.lock().await.insert(payload.selectedProfile.id, JoinAttempt::new(payload.serverId.clone())); + info!(target: "JOIN", "user {} has joined server {}", payload.selectedProfile.name, payload.serverId); + StatusCode::OK + } else { + warn!(target: "JOIN", "user {} attempted to join server {} without a valid token", payload.selectedProfile.name, payload.serverId); + StatusCode::UNAUTHORIZED + } +} + +pub async fn has_joined(State(state): State, Query(query): Query>) -> Result, StatusCode> { + let username = query.get("username").unwrap().clone(); + let server_id = query.get("serverId").unwrap(); + + let user = entities::user::Entity::find().filter( + entities::user::Column::Name.eq(username.clone()) + ).one(&state.db).await.unwrap(); + + match user { + Some(user) => { + match state.store.lock().await.get(&user.uuid) { + Some(join) => { + if Utc::now() - join.time < Duration::seconds(state.cfg.time_window as i64) + && join.server.to_lowercase() == server_id.to_lowercase() { + let response = proto::JoinResponse { + id: user.uuid, + name: username.clone(), + properties: vec![ + proto::Property { + name: "textures".into(), + value: "".into(), + signature: Some("".into()), + } + ], + }; + info!(target: "hasJOINED", "server found user -> {:?}", response); + Ok(Json(response)) + } else { + warn!(target: "hasJOINED", "server found user but join was late or for another server"); + Err(StatusCode::NOT_FOUND) + } + }, + None => { + warn!(target: "hasJOINED", "server didn't find user"); + Err(StatusCode::NOT_FOUND) + }, + } + }, + None => { + warn!(target: "hasJOINED", "invalid UUID"); + Err(StatusCode::NOT_FOUND) + }, + } +}