added tokens table and db queries, implemented most routes

This commit is contained in:
dev@ftbsc 2023-01-20 00:37:13 +01:00
parent 0f95fe2536
commit 611bf35f89
9 changed files with 290 additions and 60 deletions

View file

@ -6,10 +6,12 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4", features = ["derive"] }
tokio = { version = "1.24.2", features = ["full"] }
sea-orm = { version = "0.10", features = [ "sqlx-postgres", "sqlx-sqlite", "runtime-tokio-rustls", "macros" ] }
axum = "0.6.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tracing-subscriber = "0.3"
uuid = "1"
chrono = "0.4"

8
README.md Normal file
View file

@ -0,0 +1,8 @@
# HOW TO USE
Add to java args (on client/server)
```
-Dminecraft.api.auth.host=http://localhost:8081/auth
-Dminecraft.api.account.host=http://localhost:8081/account
-Dminecraft.api.session.host=http://localhost:8081/session
-Dminecraft.api.services.host=http://localhost:8081/services
```

View file

@ -18,27 +18,72 @@ impl MigrationTrait for Migration {
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(User::Uuid).uuid().not_null())
.col(ColumnDef::new(User::Name).string().not_null())
.col(ColumnDef::new(User::Password).string().not_null())
.col(ColumnDef::new(User::Token).string().not_null())
.to_owned(),
)
.await
.await?;
manager
.create_table(
Table::create()
.table(Token::Table)
.if_not_exists()
.col(
ColumnDef::new(Token::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(
ColumnDef::new(Token::UserId)
.integer()
.not_null()
)
.foreign_key(
ForeignKey::create()
.name("fk-user-id")
.from(Token::Table, Token::UserId)
.to(User::Table, User::Id)
)
.col(ColumnDef::new(Token::AccessToken).string().not_null())
.col(ColumnDef::new(Token::CreatedAt).date_time().not_null())
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(User::Table).to_owned())
.await
.await?;
manager
.drop_table(Table::drop().table(Token::Table).to_owned())
.await?;
Ok(())
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
enum User {
Table,
Id,
Name,
Uuid,
Password,
Token,
}
#[derive(Iden)]
enum Token {
Table,
Id,
UserId,
AccessToken,
CreatedAt,
}

View file

@ -1,5 +1,6 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.2
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.7
pub mod prelude;
pub mod token;
pub mod user;

View file

@ -1,3 +1,4 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.2
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.7
pub use super::token::Entity as Token;
pub use super::user::Entity as User;

29
src/entities/token.rs Normal file
View file

@ -0,0 +1,29 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.7
use sea_orm::entity::prelude::*;
use chrono::{DateTime, Utc};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "token")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: i32,
pub access_token: String,
pub created_at: DateTime<Utc>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "Entity",
from = "Column::UserId",
to = "Column::Id",
on_update = "NoAction",
on_delete = "NoAction"
)]
SelfRef,
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -1,4 +1,4 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.2
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.10.7
use sea_orm::entity::prelude::*;
@ -7,9 +7,9 @@ use sea_orm::entity::prelude::*;
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub uuid: Uuid,
pub name: String,
pub password: String,
pub token: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -3,12 +3,35 @@ mod entities;
use std::{net::SocketAddr, collections::HashMap, sync::Arc};
use chrono::{DateTime, Utc};
use chrono::{DateTime, Utc, Duration};
use clap::Parser;
use axum::{Router, routing::{get, post}, response::IntoResponse, Json, http::StatusCode, extract::{State, Query}};
use proto::{Error, AuthenticateRequest, AuthenticateResponse, JoinRequest, JoinResponse};
use proto::{Error, AuthenticateRequest, AuthenticateResponse, JoinRequest, JoinResponse, ValidateRequest, RefreshRequest, RefreshResponse, RegisterRequest};
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, DatabaseConnection, Database, ActiveValue::NotSet, Set};
use tokio::sync::Mutex;
use uuid::Uuid;
/// Reimplementation of legacy auth server for minecraft
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
struct ConfigArgs {
/// Connection string for database
database: String,
// /// Listen address
// #[arg(short, long, default_value_t = )]
// listen_addr: String,
/// Listen port
#[arg(short, long, default_value_t = 25556)]
listen_port: u16,
/// Valid time for join requests, in seconds
#[arg(short, long, default_value_t = 10)]
time_window: u32,
}
#[derive(Clone)]
struct JoinAttempt {
server: String,
@ -24,21 +47,32 @@ impl JoinAttempt {
#[derive(Clone)]
struct AppState {
store: Arc<Mutex<HashMap<Uuid, JoinAttempt>>>,
db: DatabaseConnection,
cfg: ConfigArgs,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt::init();
let cfg = ConfigArgs::parse();
let db = Database::connect("sqlite://./test.db").await?;
let store = Arc::new(Mutex::new(HashMap::new()));
let app = Router::new()
.route("/", get(root))
// .route("/authenticate", post(auth))
// .route("/refresh", post(refresh))
// .route("/validate", post(validate))
.route("/authenticate", post(authenticate))
.route("/validate", post(validate))
.route("/refresh", post(refresh))
// .route("/signout", post(signout))
// .route("/invalidate", post(invalidate))
.with_state(AppState { store });
.route("/join", post(join))
.route("/hasJoined", get(has_joined))
.route("/register", post(register_tmp))
.with_state(AppState { store, db, cfg });
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); // TODO parse strings from CLI
axum::Server::bind(&addr)
.serve(app.into_make_service())
@ -47,7 +81,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
async fn root(State(state): State<AppState>) -> impl IntoResponse {
async fn root() -> impl IntoResponse {
let error = Error {
error: "No route specified".into(),
errorMessage: "No route specified".into(),
@ -57,48 +91,149 @@ async fn root(State(state): State<AppState>) -> impl IntoResponse {
(StatusCode::OK, Json(error))
}
async fn authenticate(State(state): State<AppState>, Json(payload): Json<AuthenticateRequest>) -> impl IntoResponse {
let response = AuthenticateResponse {
accessToken: "".into(),
user: proto::User { id: "".parse().unwrap(), username: "".into(), properties: Some(vec![]) },
clientToken: "".into(),
availableProfiles: "".into(),
selectedProfile: "".into(),
};
async fn validate(State(state): State<AppState>, Json(payload): Json<ValidateRequest>) -> StatusCode {
let token = entities::token::Entity::find().filter(
entities::token::Column::AccessToken.eq(payload.accessToken)
).one(&state.db).await.unwrap();
(StatusCode::OK, Json(response))
}
async fn join(State(state): State<AppState>, Json(payload): Json<JoinRequest>) -> impl IntoResponse {
// TODO make sure that accessToken is valid
state.store.lock().await.insert(payload.selectedProfile, JoinAttempt::new(payload.serverId));
(StatusCode::OK, ())
}
async fn hasJoined(State(state): State<AppState>, Query(query): Query<HashMap<String, String>>) -> impl IntoResponse {
let username = query.get("username").unwrap();
let serverId = query.get("serverId").unwrap();
// TODO find UUID from username to match it with JoinAttempt
// entities::user::Entity::find().where();
let uid = uuid::uuid!("1234");
match state.store.lock().await.get(&uid) {
Some(join) => {
if join.server.to_lowercase() == serverId.to_lowercase() {
let response = JoinResponse {
id: uid,
name: username.clone(),
properties: vec![],
};
(StatusCode::OK, Json(response))
} else {
todo!()
}
},
None => todo!(), // return 404? idk error
if let Some(_t) = token {
StatusCode::NO_CONTENT
} else {
StatusCode::UNAUTHORIZED
}
}
async fn refresh(State(state): State<AppState>, Json(payload): Json<RefreshRequest>) -> Result<Json<RefreshResponse>, 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: NotSet,
user_id: NotSet,
}
).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<AppState>, Json(payload): Json<AuthenticateRequest>) -> Result<Json<AuthenticateResponse>, 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<AppState>, Json(payload): Json<JoinRequest>) -> StatusCode {
let user = entities::user::Entity::find().filter(
entities::user::Column::Uuid.eq(payload.selectedProfile)
).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, JoinAttempt::new(payload.serverId));
StatusCode::OK
} else {
StatusCode::UNAUTHORIZED
}
}
async fn has_joined(State(state): State<AppState>, Query(query): Query<HashMap<String, String>>) -> Result<Json<JoinResponse>, 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![],
};
Ok(Json(response))
} else {
Err(StatusCode::NOT_FOUND)
}
},
None => Err(StatusCode::NOT_FOUND),
}
},
None => Err(StatusCode::NOT_FOUND),
}
}
async fn register_tmp(State(state): State<AppState>, Json(payload): Json<RegisterRequest>) -> StatusCode {
let user = entities::user::ActiveModel {
id: NotSet,
name: Set(payload.user),
password: Set(payload.password),
uuid: Set(Uuid::new_v4()),
};
entities::user::Entity::insert(user).exec(&state.db).await.unwrap();
StatusCode::OK
}

View file

@ -34,7 +34,7 @@ pub struct User {
pub properties: Option<Vec<Property>>,
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Clone)]
pub struct Profile {
pub name: String,
pub id: Uuid,
@ -102,3 +102,12 @@ pub struct JoinResponse {
pub name: String,
pub properties: Vec<Property>,
}
#[derive(Serialize, Deserialize)]
pub struct RegisterRequest {
pub user: String,
pub password: String,
}