First release. Basic authentication and profiles work great

This commit is contained in:
0xf8 2023-06-21 04:13:14 -04:00
parent 5b1a0966f6
commit 9317628935
Signed by: 0xf8
GPG Key ID: 446580D758689584
45 changed files with 1410 additions and 195 deletions

View File

@ -9,16 +9,21 @@ license = "GPL3.0-or-later"
[dependencies]
anyhow = "1.0.71"
argparse = "0.2.2"
async-std = { version = "1.12.0", features = ["attributes"] }
base64 = "0.21.2"
bcrypt = "0.14.0"
colored = "2.0.0"
dialoguer = { version = "0.10.4", default-features = false, features = ["password"] }
driftwood = "0.0.7"
femme = "2.2.1"
futures = "0.3.28"
json = "0.12.4"
log = "0.4.19"
once_cell = "1.18.0"
rand = "0.8.5"
random-string = "1.0.0"
regex = "1.8.4"
rsa = "0.9.2"
serde = { version = "1.0.164", features = ["derive"] }
serde_json = "1.0.97"
@ -27,3 +32,14 @@ sqlx = { version = "0.6.3", features = ["sqlite", "runtime-async-std-native-tls"
tide = "0.16.0"
time = "0.3.22"
toml = "0.7.4"
uuid = { version = "1.3.4", features = ["v4", "fast-rng"] }
# Server
[[bin]]
name = "yggdrasil"
path = "src/main.rs"
# Database UI
[[bin]]
name = "dbtool"
path = "src/main_dbtool.rs"

14
dbtool Executable file
View File

@ -0,0 +1,14 @@
#! /usr/bin/bash
#
# Yggdrasil: Minecraft authentication server
# Copyright (C) 2023 0xf8.dev@proton.me
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
#
DATABASE_URL="sqlite:yggdrasil.db" cargo run --bin dbtool -- "$@"

52
src/dbtool/add_account.rs Normal file
View File

@ -0,0 +1,52 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::{bail, Result};
use log::info;
use structs::account::Account;
use yggdrasil::*;
use crate::dbtool::Args;
pub struct AddAccount {}
impl AddAccount {
pub async fn exec(args: Args, db: &Database) -> Result<()> {
if args.arguments.len() < 3 { bail!("Not enough arguments. add-account <email> <lang> <country>") };
// Get args
let email = args.arguments.get(0).unwrap().to_lowercase();
let lang = args.arguments.get(1).unwrap().to_lowercase();
let country = args.arguments.get(2).unwrap().to_string();
// Validate args
if !Validate::email(&email) { bail!("Invalid email; ex: \"user@example\"") }
if !Validate::lang(&lang) { bail!("Invalid language; ex: \"en-us\"") }
if !Validate::country(&country) { bail!("Invalid country; ex: \"US\"") }
// Get password
let password = Input::password().await?;
info!("Email: {email}");
info!("Lang: {lang}");
info!("Country: {country}");
info!("Password: ...{{{}}}", password.len());
// Create new account
let account = Account::new(db, email, lang, country, password).await?;
info!("New account ID: {}", account.id);
Ok(())
}
}

53
src/dbtool/add_profile.rs Normal file
View File

@ -0,0 +1,53 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::{bail, Result};
use log::info;
use structs::{account::Account, profile::Profile};
use yggdrasil::*;
use crate::dbtool::Args;
pub struct AddProfile {}
impl AddProfile {
pub async fn exec(args: Args, db: &Database) -> Result<()> {
if args.arguments.len() < 2 { bail!("Not enough arguments. add-profile <email> <name>") }
// Get args
let email = args.arguments.get(0).unwrap().to_lowercase();
let name = args.arguments.get(1).unwrap().to_string();
// Get account
let Some(account) = Account::from_email(db, email.to_owned()).await else { bail!("Account(email=\"{email}\") doesn't exist") };
// Attributes
let attributes = Input::attributes().await?;
info!("Owner ID: {}", account.id);
// Create new profile
let profile = Profile::new(db, account.to_owned(), name, attributes).await?;
info!("New profile Name: \"{}\"", profile.name);
info!("New profile ID: {}", profile.id);
info!("New profile UUID: {}", profile.uuid);
if account.selected_profile.is_none() {
info!("Setting new profile to be account's selected profile");
account.set_selected_profile(db, &profile).await?;
}
Ok(())
}
}

View File

@ -0,0 +1,46 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::str::FromStr;
use anyhow::{bail, Result};
use log::info;
use structs::profile::Profile;
use yggdrasil::*;
use crate::dbtool::Args;
use crate::util::structs::account::Account;
pub struct AttachProfile {}
impl AttachProfile {
pub async fn exec(args: Args, db: &Database) -> Result<()> {
if args.arguments.len() < 2 { bail!("Not enough arguments. attach-profile <account-id> <profile-id>") }
// Get ids
let account_id = i64::from_str(args.arguments.get(0).unwrap())?;
let profile_id = i64::from_str(args.arguments.get(1).unwrap())?;
// Get account
let Some(account) = Account::from_id(db, account_id).await else {
bail!("Account(id = {account_id}) doesn't exist")
};
// Get profile
let Some(profile) = Profile::from_id(db, profile_id).await else {
bail!("Profile(id = {profile_id}) doesn't exist")
};
account.set_selected_profile(db, &profile).await
}
}

View File

@ -9,11 +9,30 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use std::str::FromStr;
use yggdrasil::Database;
use anyhow::{bail, Result};
use log::info;
pub async fn refresh(req: Request<Database>) -> Result {
Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
}
use structs::account::Account;
use yggdrasil::*;
use crate::dbtool::Args;
pub struct DelAccount {}
impl DelAccount {
pub async fn exec(args: Args, db: &Database) -> Result<()> {
if args.arguments.len() < 1 { bail!("Not enough arguments. del-account <id>") }
// Get id
let id = i64::from_str(args.arguments.get(0).unwrap())?;
// Delete account
let email = Account::del(db, id).await?;
info!("Deleted account(email = \"{email}\")");
Ok(())
}
}

View File

@ -9,11 +9,30 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use std::str::FromStr;
use yggdrasil::Database;
use anyhow::{bail, Result};
use log::info;
pub async fn invalidate(req: Request<Database>) -> Result {
Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
}
use structs::profile::Profile;
use yggdrasil::*;
use crate::dbtool::Args;
pub struct DelProfile {}
impl DelProfile {
pub async fn exec(args: Args, db: &Database) -> Result<()> {
if args.arguments.len() < 1 { bail!("Not enough arguments. del-profile <id>") }
// Get id
let id = i64::from_str(args.arguments.get(0).unwrap())?;
// Delete profile
let uuid = Profile::del(db, id).await?;
info!("Deleted profile(uuid = \"{uuid}\")");
Ok(())
}
}

87
src/dbtool/dump.rs Normal file
View File

@ -0,0 +1,87 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::{bail, Result};
use log::info;
use yggdrasil::*;
use yggdrasil::structs::*;
use crate::dbtool::Args;
pub struct Dump {}
impl Dump {
async fn dump_accounts(db: &Database) -> Result<()> {
let r = sqlx::query_as!(account::AccountRaw, "SELECT * FROM accounts")
.fetch_all(&db.pool)
.await?;
info!("[ Got {} records ]", r.len());
Ok(for a in r {
info!("{:#?}", a.complete(db).await)
})
}
async fn dump_profiles(db: &Database) -> Result<()> {
let r = sqlx::query_as!(profile::ProfileRaw, "SELECT * FROM profiles")
.fetch_all(&db.pool)
.await?;
info!("[ Got {} records ]", r.len());
Ok(for p in r {
info!("{:#?}", p.complete(db).await.to_simple())
})
}
async fn dump_sessions(db: &Database) -> Result<()> {
let r = sqlx::query_as!(session::SessionRaw, "SELECT * FROM sessions")
.fetch_all(&db.pool)
.await?;
info!("[ Got {} records ]", r.len());
Ok(for s in r {
info!("{:#?}", s.complete(db).await)
})
}
async fn dump_tokens(db: &Database) -> Result<()> {
let r = sqlx::query_as!(token::TokenRaw, "SELECT * FROM tokens")
.fetch_all(&db.pool)
.await?;
info!("[ Got {} records ]", r.len());
Ok(for t in r {
info!("{:#?}", t.complete(db).await)
})
}
pub async fn exec(args: Args, db: &Database) -> Result<()> {
if args.arguments.len() < 1 { bail!("Not enough arguments. dump <table>") }
let table = args.arguments.get(0).unwrap().to_lowercase();
match table.as_str() {
"accounts" => Self::dump_accounts(db).await?,
"profiles" => Self::dump_profiles(db).await?,
"sessions" => Self::dump_sessions(db).await?,
"tokens" => Self::dump_tokens(db).await?,
_ => bail!("Invalid table \"{table}\". Tables: accounts, profiles, sessions, tokens")
}
Ok(())
}
}

76
src/dbtool/mod.rs Normal file
View File

@ -0,0 +1,76 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::{bail, Result};
use argparse::{List, parser::ArgumentParser, Store, StoreTrue};
use log::{debug, info};
use yggdrasil::*;
mod dump;
mod search;
mod add_account;
mod add_profile;
mod del_account;
mod del_profile;
mod attach_profile;
#[derive(Debug, Clone)]
pub struct Args {
pub command: String,
pub arguments: Vec<String>,
}
pub async fn start(db: &Database) -> Result<()> {
let mut args = Args {
command: String::new(),
arguments: vec![],
};
{
let mut parser = ArgumentParser::new();
parser.set_description("Database tool for Yggdrasil");
parser.refer(&mut args.command)
.add_argument("command", Store, "Command to run")
.required();
parser.refer(&mut args.arguments)
.add_argument("arguments", List, "Arguments for command");
parser.parse_args_or_exit();
}
match args.command.to_lowercase().as_str() {
"dump" => dump::Dump::exec(args, &db).await?,
"search" => search::Search::exec(args, &db).await?,
"addaccount" |
"add-account" => add_account::AddAccount::exec(args, &db).await?,
"addprofile" |
"add-profile" => add_profile::AddProfile::exec(args, &db).await?,
"delaccount" |
"del-account" => del_account::DelAccount::exec(args, &db).await?,
"delprofile" |
"del-profile" => del_profile::DelProfile::exec(args, &db).await?,
"attachprofile" |
"attach-profile" => attach_profile::AttachProfile::exec(args, &db).await?,
_ => bail!("Command doesn't exist")
}
Ok(())
}

111
src/dbtool/search.rs Normal file
View File

@ -0,0 +1,111 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::str::FromStr;
use anyhow::{bail, Result};
use log::{info, warn};
use yggdrasil::*;
use yggdrasil::structs::account::Account;
use crate::dbtool::Args;
use crate::util::structs::profile::Profile;
pub struct Search {}
impl Search {
// Account id
async fn search_accountid(db: &Database, query: Vec<String>) -> Result<()> {
Ok(for q in query {
let id = match i64::from_str(&q) {
Ok(id) => id,
Err(_) => bail!("id({q}) isn't a valid i64")
};
match Account::from_id(db, id).await {
None => warn!("Account(id = {id}) doesn't exist"),
Some(a) => info!("{a:#?}")
}
})
}
// Profile id
async fn search_profileid(db: &Database, query: Vec<String>) -> Result<()> {
Ok(for q in query {
let id = match i64::from_str(&q) {
Ok(id) => id,
Err(_) => bail!("id({q}) isn't a valid i64")
};
match Profile::from_id(db, id).await {
None => warn!("Profile(id = {id}) doesn't exist"),
Some(p) => info!("{:#?}", p.to_simple())
}
})
}
// Account name
async fn search_email(db: &Database, query: Vec<String>) -> Result<()> {
Ok(for q in query {
match Account::from_email(db, q.to_string()).await {
None => warn!("Account(email = \"{q}\") doesn't exist"),
Some(a) => info!("{a:#?}")
}
})
}
// Profile name
async fn search_name(db: &Database, query: Vec<String>) -> Result<()> {
Ok(for q in query {
match Profile::from_name(db, q.to_string()).await {
None => warn!("Profile(name = \"{q}\") doesn't exist"),
Some(p) => info!("{:#?}", p.to_simple())
}
})
}
// Profile uuid
async fn search_uuid(db: &Database, query: Vec<String>) -> Result<()> {
Ok(for q in query {
match Profile::from_uuid(db, q.to_string()).await {
None => warn!("Profile(uuid = \"{q}\") doesn't exist"),
Some(p) => info!("{:#?}", p.to_simple())
}
})
}
pub async fn exec(args: Args, db: &Database) -> Result<()> {
if args.arguments.len() < 2 { bail!("Not enough arguments. search <type> <query> [query..]\ntype: account-id | profile-id | email | name | uuid") }
let query_type = args.arguments.get(0).unwrap().to_lowercase();
let queries = args.arguments[1..args.arguments.len()].to_vec();
match query_type.as_str() {
"accountid" |
"account-id" => Self::search_accountid(db, queries).await?,
"profileid" |
"profile-id" => Self::search_profileid(db, queries).await?,
"email" => Self::search_email(db, queries).await?,
"name" => Self::search_name(db, queries).await?,
"uuid" => Self::search_uuid(db, queries).await?,
_ => bail!("Invalid type \"{query_type}\"")
}
Ok(())
}
}

View File

@ -12,7 +12,8 @@
#![feature(fs_try_exists)]
use anyhow::{bail, Result};
use log::{debug, error, info, log, trace, warn};
use log::{info, warn};
use log::LevelFilter::Debug;
use yggdrasil::*;
@ -26,7 +27,11 @@ async fn main() -> Result<()> {
}
// Start logger
femme::start();
if std::env::var("DEBUG").unwrap_or(String::new()).to_lowercase() == "on" {
femme::with_level(Debug);
} else {
femme::start();
}
// Load config
let config = Config::load()?;
@ -36,9 +41,10 @@ async fn main() -> Result<()> {
let db = Database::init(config).await?;
info!("Database URL: {}", std::env::var("DATABASE_URL")?);
let wrapper = DatabaseWrapper { db };
// Start server
let server_thread = async_std::task::spawn(server::start(db));
server_thread.await?;
server::start(&wrapper.db).await?;
warn!("Server stopped!");

50
src/main_dbtool.rs Normal file
View File

@ -0,0 +1,50 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#![feature(fs_try_exists)]
use anyhow::{bail, Result};
use log::{debug, error, info, log, trace, warn};
use log::LevelFilter::{Debug, Info};
use yggdrasil::*;
mod util;
mod dbtool;
#[async_std::main]
async fn main() -> Result<()> {
// Early catch
if std::env::var("DATABASE_URL").is_err() {
bail!("DATABASE_URL needs to be set.")
}
// Start logger
if std::env::var("DEBUG").unwrap_or(String::new()).to_lowercase() == "on" {
femme::with_level(Debug);
} else {
femme::with_level(Info);
}
// Load config
let config = Config::load()?;
// Load database
let db = Database::init(config).await?;
match dbtool::start(&db).await {
Ok(_) => (),
Err(e) => error!("{e}")
}
// Cleanup
Ok(log::logger().flush())
}

View File

@ -10,7 +10,7 @@
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use tide::{Request, Result};
use yggdrasil::Database;

View File

@ -10,7 +10,7 @@
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use tide::{Request, Result};
use yggdrasil::Database;

View File

@ -9,32 +9,35 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use log::{debug, info};
use tide::{prelude::*, Request, Result};
use yggdrasil::*;
use yggdrasil::errors::YggdrasilError;
use yggdrasil::structs::{Account::Account, Cape::Cape, Token::Token};
use yggdrasil::structs::{account::Account, cape::Cape, token::Token};
#[derive(Deserialize, Debug)]
struct Agent {
pub name: String,
pub version: i64
pub version: i64,
}
#[derive(Deserialize, Debug)]
struct AuthenticateBody {
pub agent: Agent,
pub username: String,
pub password: String, // hashed?
pub password: String,
#[serde(rename = "clientToken")]
pub client_token: Option<String>,
#[serde(rename = "requestUser")]
pub request_user: Option<bool>
pub request_user: Option<bool>,
}
pub async fn authenticate(mut req: Request<Database>) -> Result {
let Ok(body) = req.body_json::<AuthenticateBody>().await else {
return Err(YggdrasilError::new_bad_request("Bad Request").into());
return Err(YggdrasilError::new_bad_request("Credentials can not be null.").into());
};
// Check current agent
@ -45,14 +48,15 @@ pub async fn authenticate(mut req: Request<Database>) -> Result {
// Get account
let account = Account::from_email(req.state(), body.username).await;
// Account doesn't exist
let Some(account) = account else {
return Err(YggdrasilError::new_forbidden("Invalid credentials. Invalid username or password.").into())
// Account doesn't exist
return Err(YggdrasilError::new_unauthorized("Invalid credentials. Invalid username or password.").into());
};
// Password incorrect
if account.password_hash != body.password {
return Err(YggdrasilError::new_forbidden("Invalid credentials. Invalid username or password.").into());
// Verify password
if !bcrypt::verify(body.password, &account.password_hash)? {
// Password incorrect
return Err(YggdrasilError::new_unauthorized("Invalid credentials. Invalid username or password.").into());
}
// Response
@ -61,18 +65,21 @@ pub async fn authenticate(mut req: Request<Database>) -> Result {
Some(t) => t
};
// New token
let Some(token) = Token::new(req.state(), account.to_owned(), client_token).await else {
return Err(YggdrasilError::new_bad_request("Couldn't create new token").into())
};
let mut response = json!({
"clientToken": client_token,
"accessToken": "", // TODO: register_token
"availableProfiles": [], // TODO: get account profiles
"clientToken": token.client,
"accessToken": token.access,
"availableProfiles": account.get_all_profiles(req.state()).await.unwrap_or(Vec::new()),
});
// Give selected profile if it exists
if account.selected_profile.is_some() {
let profile = account.to_owned().selected_profile.unwrap();
if let Some(profile) = account.selected_profile.to_owned() {
response["selectedProfile"] = json!({
"uuid": profile.uuid,
"id": profile.uuid,
"name": profile.name,
"name_history": profile.name_history,
"skin_variant": profile.skin_variant,
@ -80,7 +87,7 @@ pub async fn authenticate(mut req: Request<Database>) -> Result {
Some(capes) => Cape::capes_to_string(capes),
None => "".to_string()
},
"active_cape": profile.active_cape.unwrap(),
"active_cape": profile.active_cape,
"attributes": profile.attributes.to_json()
});
}

View File

@ -0,0 +1,48 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use yggdrasil::Database;
use yggdrasil::errors::YggdrasilError;
use yggdrasil::structs::token::Token;
#[derive(Deserialize, Debug)]
struct InvalidateBody {
#[serde(rename = "accessToken")]
access_token: String,
#[serde(rename = "clientToken")]
client_token: String,
}
pub async fn invalidate(mut req: Request<Database>) -> Result {
let Ok(body) = req.body_json::<InvalidateBody>().await else {
// No credentials
return Err(YggdrasilError::new_bad_request("Credentials can not be null.").into())
};
let Some(token) = Token::from_access_token(req.state(), body.access_token).await else {
// Token doesn't exist
return Err(YggdrasilError::new_unauthorized("Invalid token.").into())
};
// Verify token
if !token.validate_with(req.state(), body.client_token, false).await? {
return Err(YggdrasilError::new_unauthorized("Invalid token.").into())
}
// Delete token
token.delete(req.state()).await?;
Ok("".into())
}

View File

@ -0,0 +1,84 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use log::debug;
use tide::{prelude::*, Request, Result};
use yggdrasil::Database;
use yggdrasil::errors::YggdrasilError;
use yggdrasil::structs::{cape::Cape, token::Token};
#[derive(Deserialize, Debug)]
struct RefreshBody {
#[serde(rename = "accessToken")]
access_token: String,
#[serde(rename = "clientToken")]
client_token: String,
#[serde(rename = "requestUser")]
pub request_user: Option<bool>,
}
pub async fn refresh(mut req: Request<Database>) -> Result {
let Ok(body) = req.body_json::<RefreshBody>().await else {
// No credentials
return Err(YggdrasilError::new_bad_request("Credentials can not be null.").into())
};
debug!("accessToken: {}", body.access_token);
debug!("clientToken: {}", body.client_token);
let Some(token) = Token::from_access_token(req.state(), body.access_token).await else {
// Token doesn't exist
return Err(YggdrasilError::new_unauthorized("Invalid token.").into())
};
// Verify token
if !token.validate_with(req.state(), body.client_token, false).await? {
return Err(YggdrasilError::new_unauthorized("Invalid token.").into())
}
// Delete old token
token.delete(req.state()).await?;
let Some(new_token) = Token::new(req.state(), token.account, token.client).await else {
return Err(YggdrasilError::new_bad_request("Couldn't create new token").into())
};
// Create response
let mut response = json!({
"accessToken": new_token.access,
"clientToken": new_token.client
});
// Give selected profile if it exists
if let Some(profile) = new_token.account.selected_profile.to_owned() {
response["selectedProfile"] = json!({
"id": profile.uuid,
"name": profile.name,
"name_history": profile.name_history,
"skin_variant": profile.skin_variant,
"capes": match profile.capes {
Some(capes) => Cape::capes_to_string(capes),
None => "".to_string()
},
"active_cape": profile.active_cape,
"attributes": profile.attributes.to_json()
});
}
// Give user if requested
if body.request_user.unwrap_or(false) { response["user"] = new_token.account.to_user() }
Ok(response.into())
}

View File

@ -0,0 +1,48 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use yggdrasil::Database;
use yggdrasil::errors::YggdrasilError;
use yggdrasil::structs::account::Account;
use yggdrasil::structs::token::Token;
#[derive(Deserialize, Debug)]
struct SignoutBody {
pub username: String,
pub password: String
}
pub async fn signout(mut req: Request<Database>) -> Result {
let Ok(body) = req.body_json::<SignoutBody>().await else {
// No credentials
return Err(YggdrasilError::new_bad_request("Credentials can not be null.").into())
};
// Get account
let Some(account) = Account::from_email(req.state(), body.username).await else {
// Account doesn't exist
return Err(YggdrasilError::new_unauthorized("Invalid credentials. Invalid username or password.").into())
};
// Verify password
if !bcrypt::verify(body.password, &account.password_hash)? {
// Password incorrect
return Err(YggdrasilError::new_unauthorized("Invalid credentials. Invalid username or password.").into());
}
// Delete all tokens
Token::delete_all_from(req.state(), account).await?;
Ok("".into())
}

View File

@ -0,0 +1,46 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use yggdrasil::Database;
use yggdrasil::errors::YggdrasilError;
use yggdrasil::structs::token::Token;
#[derive(Deserialize, Debug)]
struct ValidateBody {
#[serde(rename = "accessToken")]
access_token: String,
#[serde(rename = "clientToken")]
client_token: String,
}
pub async fn validate(mut req: Request<Database>) -> Result {
let Ok(body) = req.body_json::<ValidateBody>().await else {
// No credentials
return Err(YggdrasilError::new_illegal_argument("Credentials can not be null.").into())
};
// Get token
let Some(token) = Token::from_access_token(req.state(), body.access_token).await else {
// Token doesn't exist
return Err(YggdrasilError::new_forbidden("Token expired.").into())
};
// Verify token
if !token.validate_with(req.state(), body.client_token, false).await? {
return Err(YggdrasilError::new_forbidden("Token expired.").into())
}
Ok("".into())
}

View File

@ -20,8 +20,8 @@ pub fn nest(db: Database) -> tide::Server<Database> {
let mut nest = tide::with_state(db.to_owned());
nest.at("/").get(authlib_meta);
nest.at("/authserver").nest(super::authserver::nest(db.to_owned()));
nest.at("/sessionserver").nest(super::sessionserver::nest(db.to_owned()));
nest.at("/authserver").nest(super::auth::nest(db.to_owned()));
nest.at("/sessionserver").nest(super::session::nest(db.to_owned()));
nest
}

View File

@ -1,19 +0,0 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use yggdrasil::Database;
pub async fn signout(req: Request<Database>) -> Result {
Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
}

View File

@ -10,7 +10,7 @@
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use tide::{Request, Result};
use yggdrasil::Database;
@ -18,7 +18,6 @@ pub async fn upload_cape(req: Request<Database>) -> Result {
Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
}
pub async fn delete_cape(req: Request<Database>) -> Result {
Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
}

View File

@ -9,25 +9,29 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use log::info;
use log::debug;
use tide::{Request, Response, utils::After};
use yggdrasil::*;
mod account;
mod authserver;
mod auth;
mod authlib;
mod minecraft;
mod sessionserver;
mod session;
pub async fn start(db: Database) -> anyhow::Result<()> {
pub async fn start(db: &Database) -> anyhow::Result<()> {
let mut app = tide::with_state(db.to_owned());
// Error handling middleware
app.with(After(|mut res: Response| async move {
if let Some(err) = res.downcast_error::<errors::YggdrasilError>() {
debug!("{:?}", err.to_owned());
let body = err.to_json();
res.set_status(err.2);
let status = err.2;
res.set_status(status);
res.set_body(body);
// TODO: pass through
@ -40,17 +44,20 @@ pub async fn start(db: Database) -> anyhow::Result<()> {
}));
// Index
app.at("/").get(|mut req: Request<Database>| async move {
req.append_header("x-authlib-injector-api-location", "/authlib/");
Ok("Yggdrasil")
app.at("/").get(|req: Request<Database>| async move {
let res = Response::builder(200)
.header("x-authlib-injector-api-location", format!("{}/authlib/", req.state().config.external_base_url))
.build();
Ok(res)
});
// Routes
app.at("/authlib/").nest(authlib::nest(db.to_owned()));
app.at("/account/").nest(account::nest(db.to_owned()));
app.at("/minecraft/").nest(minecraft::nest(db.to_owned()));
app.at("/authserver/").nest(authserver::nest(db.to_owned()));
app.at("/sessionserver/").nest(sessionserver::nest(db.to_owned()));
app.at("/auth/").nest(auth::nest(db.to_owned()));
app.at("/session/").nest(session::nest(db.to_owned()));
// Listen
app.listen(format!("{}:{}", &db.config.address, &db.config.port)).await?;

View File

@ -0,0 +1,58 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use yggdrasil::Database;
use yggdrasil::errors::YggdrasilError;
use yggdrasil::structs::profile::Profile;
use yggdrasil::structs::session::Session;
use yggdrasil::structs::textured_object::TexturedObject;
#[derive(Deserialize, Debug)]
struct HasJoinedBody {
pub username: String,
#[serde(rename = "serverId")]
pub server_id: String,
pub ip: Option<String>,
}
pub async fn has_joined(mut req: Request<Database>) -> Result {
let Ok(body) = req.body_json::<HasJoinedBody>().await else {
// No args
return Err(YggdrasilError::new_bad_request("One or more required fields was missing.").into())
};
// Get profile
let Some(profile) = Profile::from_name(req.state(), body.username).await else {
return Err(YggdrasilError::new_bad_request("Profile does not exist.").into())
};
// Get session
let Some(session) = Session::from_profile(req.state(), &profile).await else {
return Err(YggdrasilError::new_bad_request("Session does not exist.").into())
};
// Check IP if requested
if let Some(ip) = body.ip {
if ip != session.ip_addr {
return Err(YggdrasilError::new_forbidden("IP address does not match.").into())
}
}
// Remove session
session.delete(req.state()).await?;
Ok(TexturedObject::from_profile(req.state(), &profile).await.into())
}

View File

@ -0,0 +1,60 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use tide::{prelude::*, Request, Result};
use yggdrasil::Database;
use yggdrasil::errors::YggdrasilError;
use yggdrasil::structs::session::Session;
use yggdrasil::structs::token::Token;
#[derive(Deserialize, Debug)]
struct JoinBody {
#[serde(rename = "accessToken")]
pub access_token: String,
#[serde(rename = "selectedProfile")]
pub profile_uuid: String,
#[serde(rename = "serverId")]
pub server_id: String
}
pub async fn join(mut req: Request<Database>) -> Result {
let Ok(body) = req.body_json::<JoinBody>().await else {
return Err(YggdrasilError::new_bad_request("Bad Request").into())
};
let Some(token) = Token::from_access_token(req.state(), body.access_token).await else {
// Token doesnt exist
return Err(YggdrasilError::new_unauthorized("Invalid token.").into())
};
if !token.validate(req.state(), false).await? {
// Invalid token
return Err(YggdrasilError::new_unauthorized("Invalid token.").into())
}
let Some(profile) = token.account.selected_profile.to_owned() else {
// No selected profile
return Err(YggdrasilError::new_unauthorized("Invalid token.").into())
};
if body.profile_uuid != profile.uuid {
// UUID doesn't match
return Err(YggdrasilError::new_unauthorized("Invalid token.").into())
}
Session::create(req.state(), &profile, body.server_id, req.remote().unwrap().to_string()).await?;
Ok("".into())
}

View File

@ -22,8 +22,8 @@ pub fn nest(db: Database) -> tide::Server<Database> {
info!("Loading nest");
let mut nest = tide::with_state(db);
nest.at("hasJoined").get(has_joined::has_joined);
nest.at("join").post(join::join);
nest.at("minecraft/hasJoined").get(has_joined::has_joined);
nest.at("minecraft/join").post(join::join);
nest.at("profile/:uuid").get(profile::profile);
nest

View File

@ -0,0 +1,40 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use log::debug;
use tide::{prelude::*, Request, Result};
use yggdrasil::Database;
use yggdrasil::errors::YggdrasilError;
use yggdrasil::structs::profile::Profile;
use yggdrasil::structs::textured_object::TexturedObject;
use yggdrasil::structs::token::Token;
// TODO: unsigned?
pub async fn profile(mut req: Request<Database>) -> Result {
let Ok(uuid) = req.param("uuid") else {
// No uuid
debug!("No uuid");
return Err(YggdrasilError::new_bad_request("One or more required fields was missing.").into())
};
let uuid = match uuid.find("-") {
None => Token::rehyphenate(uuid.to_string()),
Some(_) => uuid.to_string(),
};
let Some(profile) = Profile::from_uuid(req.state(), uuid).await else {
return Err(YggdrasilError::new_bad_request("Profile does not exist").into())
};
Ok(TexturedObject::from_profile(req.state(), &profile).await.into())
}

View File

@ -1,19 +0,0 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use yggdrasil::Database;
pub async fn has_joined(req: Request<Database>) -> Result {
Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
}

View File

@ -1,19 +0,0 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use yggdrasil::Database;
pub async fn join(req: Request<Database>) -> Result {
Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
}

View File

@ -1,19 +0,0 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use yggdrasil::Database;
pub async fn profile(req: Request<Database>) -> Result {
Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
}

View File

@ -14,6 +14,8 @@ use std::str::FromStr;
use std::time::Duration;
use anyhow::Result;
use futures::executor;
use log::debug;
use sqlx::{ConnectOptions, SqlitePool};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
@ -40,3 +42,15 @@ impl Database {
})
}
}
pub struct DatabaseWrapper {
pub db: Database
}
impl Drop for DatabaseWrapper {
fn drop(&mut self) {
debug!("Dropping database");
executor::block_on(self.db.pool.close());
}
}

View File

@ -13,7 +13,7 @@ use std::{error::Error, fmt};
use serde_json::json;
use crate::errors::YggdrasilErrorType::{BadRequestException, BaseYggdrasilException, ForbiddenOperationException, IllegalArgumentException};
use YggdrasilErrorType::*;
#[derive(Debug)]
pub struct YggdrasilError(pub YggdrasilErrorType, pub String, pub u16, pub bool);
@ -23,6 +23,7 @@ pub struct YggdrasilError(pub YggdrasilErrorType, pub String, pub u16, pub bool)
pub enum YggdrasilErrorType {
BaseYggdrasilException,
ForbiddenOperationException,
UnauthorizedOperationException,
BadRequestException,
IllegalArgumentException,
}
@ -34,6 +35,7 @@ impl fmt::Display for YggdrasilError {
use YggdrasilErrorType::*;
match self.0 {
UnauthorizedOperationException |
ForbiddenOperationException => write!(f, "FORBIDDEN"),
BadRequestException => write!(f, "BAD_REQUEST"),
_ => write!(f, "INTERNAL_SERVER_ERROR"),
@ -60,6 +62,15 @@ impl YggdrasilError {
}
}
pub fn new_unauthorized(msg: &str) -> Self {
Self {
0: UnauthorizedOperationException,
1: msg.to_string(),
2: 401,
3: true,
}
}
pub fn new_forbidden(msg: &str) -> Self {
Self {
0: ForbiddenOperationException,
@ -86,5 +97,4 @@ impl YggdrasilError {
3: false
}
}
}

58
src/util/input.rs Normal file
View File

@ -0,0 +1,58 @@
/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use dialoguer::{MultiSelect, Password};
use dialoguer::theme::ColorfulTheme;
use super::structs::profile_attributes::ProfileAttributesSimple;
pub struct Input {}
impl Input {
pub async fn password() -> Result<String> {
let theme = ColorfulTheme::default();
let mut password = Password::with_theme(&theme);
password.with_prompt("Password");
Ok(password.interact()?)
}
pub async fn attributes() -> Result<ProfileAttributesSimple> {
let theme = ColorfulTheme::default();
let mut select = MultiSelect::with_theme(&theme);
select.with_prompt("Attributes");
select.items(&["Can chat", "Can play multiplayer", "Can play realms", "Use profanity filter"]);
select.defaults(&[true, true, true, false]);
let mut attr = ProfileAttributesSimple {
can_chat: false,
can_play_multiplayer: false,
can_play_realms: false,
use_filter: false,
};
for a in select.interact()? {
match a {
0 => attr.can_chat = true,
1 => attr.can_play_multiplayer = true,
2 => attr.can_play_realms = true,
3 => attr.use_filter = true,
_ => ()
}
}
Ok(attr)
}
}

View File

@ -9,14 +9,25 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::time::UNIX_EPOCH;
pub use config::Config;
pub use database::Database;
pub use database::DatabaseWrapper;
pub use input::Input;
pub use validate::Validate;
mod config;
// TODO: fix signing
// https://github.com/RustCrypto/RSA/blob/master/tests/proptests.rs
// mod signing;
mod database;
mod input;
mod validate;
pub mod errors;
pub mod structs;
// TODO: fix signing
// https://github.com/RustCrypto/RSA/blob/master/tests/proptests.rs
// mod signing;
pub fn get_unix_timestamp() -> u128 {
std::time::SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards?!").as_millis()
}

View File

@ -9,6 +9,8 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use log::debug;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
@ -16,6 +18,7 @@ use structs::profile::{Profile, ProfileRaw};
use crate::*;
// TODO: 2FA
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Account {
pub id: i64,
@ -34,7 +37,7 @@ impl Account {
match record {
Ok(r) => Some(r.complete(db).await),
Err(_) => None
Err(e) => { debug!("{e}"); None },
}
}
@ -45,7 +48,7 @@ impl Account {
match record {
Ok(r) => Some(r.complete(db).await),
Err(_) => None,
Err(e) => { debug!("{e}"); None },
}
}
@ -60,10 +63,43 @@ impl Account {
}
Some(collection)
} // oh boy
Err(_) => None
Err(e) => { debug!("{e}"); None },
}
}
pub async fn set_selected_profile(&self, db: &Database, profile: &Profile) -> Result<()> {
sqlx::query!("UPDATE accounts SET selected_profile = $1 WHERE id = $2", profile.id, self.id)
.execute(&db.pool)
.await?;
Ok(())
}
pub async fn new(db: &Database, email: String, language: String, country: String, password: String) -> Result<Account> {
let password_hash = bcrypt::hash(password, 12)?;
let r = sqlx::query!("INSERT INTO accounts(email, language, country, password_hash) VALUES ($1, $2, $3, $4) RETURNING (id)", email, language, country, password_hash)
.fetch_one(&db.pool)
.await?;
Ok(Account {
id: r.id,
email,
password_hash,
language,
country,
selected_profile: None,
})
}
pub async fn del(db: &Database, id: i64) -> Result<String> {
let r = sqlx::query!("DELETE FROM accounts WHERE id = $1 RETURNING (email)", id)
.fetch_one(&db.pool)
.await?;
Ok(r.email)
}
pub fn to_user(&self) -> Value {
json!({
"id": self.id,

View File

@ -9,6 +9,7 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use log::debug;
use serde::{Deserialize, Serialize};
use crate::*;
@ -29,7 +30,7 @@ impl BlockedServer {
match record {
Ok(r) => Some(r),
Err(_) => None,
Err(e) => { debug!("{e}"); None },
}
}
}

View File

@ -9,6 +9,7 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use log::debug;
use serde::{Deserialize, Serialize};
use crate::*;
@ -28,7 +29,7 @@ impl Cape {
match record {
Ok(r) => Some(r),
Err(_) => None,
Err(e) => { debug!("{e}"); None },
}
}

View File

@ -9,12 +9,16 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::Result;
use log::debug;
use serde::{Deserialize, Serialize};
use structs::{cape::Cape, profile_attributes::ProfileAttributes};
use structs::{cape::Cape, profile_attributes::{ProfileAttributes, ProfileAttributesSimple}};
use crate::*;
use super::account::Account;
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Profile {
pub id: i64,
@ -30,7 +34,7 @@ pub struct Profile {
pub capes: Option<Vec<Cape>>,
pub active_cape: Option<Cape>,
pub attributes: ProfileAttributes,
pub attributes: ProfileAttributesSimple,
}
impl Profile {
@ -41,7 +45,7 @@ impl Profile {
match record {
Ok(r) => Some(r.complete(db).await),
Err(_) => None,
Err(e) => { debug!("{e}"); None },
}
}
@ -52,7 +56,7 @@ impl Profile {
match record {
Ok(r) => Some(r.complete(db).await),
Err(_) => None
Err(e) => { debug!("{e}"); None },
}
}
@ -63,7 +67,54 @@ impl Profile {
match record {
Ok(r) => Some(r.complete(db).await),
Err(_) => None
Err(e) => { debug!("{e}"); None },
}
}
pub async fn get_owner(&self, db: &Database) -> Option<Account> {
Account::from_id(db, self.owner).await
}
pub async fn new(db: &Database, owner: Account, name: String, attr: ProfileAttributesSimple) -> Result<Profile> {
let created = (get_unix_timestamp() / 1000) as i64;
let uuidv4 = uuid::Uuid::new_v4().to_string();
let attributes = attr.to_json().to_string();
let r = sqlx::query!("INSERT INTO profiles(uuid, created, owner, name, name_history, skin_variant, attributes) VALUES ($1, $2, $3, $4, $4, $5, $6) RETURNING (id)",
uuidv4, created, owner.id, name, "NONE", attributes)
.fetch_one(&db.pool)
.await?;
Ok(Profile {
id: r.id,
uuid: uuidv4,
created,
owner: owner.id,
name: name.to_owned(),
name_history: name,
skin_variant: String::from("NONE"),
capes: None,
active_cape: None,
attributes: attr,
})
}
pub async fn del(db: &Database, id: i64) -> Result<String> {
let r = sqlx::query!("DELETE FROM profiles WHERE id = $1 RETURNING (uuid)", id)
.fetch_one(&db.pool)
.await?;
Ok(r.uuid)
}
pub fn to_simple(self) -> ProfileSimple {
ProfileSimple {
id: self.id,
owner: self.owner,
uuid: self.uuid,
name: self.name,
active_cape: self.active_cape,
attributes: self.attributes,
}
}
@ -93,6 +144,19 @@ impl Profile {
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ProfileSimple {
pub id: i64,
pub owner: i64,
pub uuid: String,
pub name: String,
pub active_cape: Option<Cape>,
pub attributes: ProfileAttributesSimple
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ProfileRaw {
pub id: i64,
@ -137,8 +201,8 @@ impl ProfileRaw {
None => None,
Some(active_cape) => Cape::from_id(db, active_cape).await,
},
attributes: serde_json::from_str(self.attributes.as_str())
.expect("Couldn't parse profile attributes"),
attributes: serde_json::from_str::<ProfileAttributes>(self.attributes.as_str())
.expect("Couldn't parse profile attributes").to_simple(),
}
}
}

View File

@ -9,19 +9,52 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use serde_json::{json, Value};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Deserialize, Debug)]
pub struct AttributeEnabled {
pub enabled: bool
}
#[derive(Deserialize, Debug)]
pub struct ProfanityFilter {
#[serde(rename = "profanityFilterOn")]
pub profanity_filter: bool
}
#[derive(Deserialize, Debug)]
pub struct ProfileAttributesPrivileges {
#[serde(rename = "onlineChat")]
pub online_chat: AttributeEnabled,
#[serde(rename = "multiplayerServer")]
pub multiplayer_server: AttributeEnabled,
#[serde(rename = "multiplayerRealms")]
pub multiplayer_realms: AttributeEnabled,
pub telemetry: AttributeEnabled,
}
#[derive(Deserialize, Debug)]
pub struct ProfileAttributes {
pub privileges: ProfileAttributesPrivileges,
#[serde(rename = "profanityFilterPreferences")]
pub profanity_filter: ProfanityFilter,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ProfileAttributes {
pub struct ProfileAttributesSimple {
pub can_chat: bool,
pub can_play_multiplayer: bool,
pub can_play_realms: bool,
pub use_filter: bool,
}
impl ProfileAttributes {
impl ProfileAttributesSimple {
pub fn to_json(&self) -> Value {
json!({
"privileges": {
@ -35,4 +68,27 @@ impl ProfileAttributes {
}
})
}
pub fn to_full(&self) -> ProfileAttributes {
ProfileAttributes {
privileges: ProfileAttributesPrivileges {
online_chat: AttributeEnabled { enabled: self.can_chat },
multiplayer_server: AttributeEnabled { enabled: self.can_play_multiplayer },
multiplayer_realms: AttributeEnabled { enabled: self.can_play_realms },
telemetry: AttributeEnabled { enabled: false },
},
profanity_filter: ProfanityFilter { profanity_filter: self.use_filter },
}
}
}
impl ProfileAttributes {
pub fn to_simple(&self) -> ProfileAttributesSimple {
ProfileAttributesSimple {
can_chat: self.privileges.online_chat.enabled,
can_play_multiplayer: self.privileges.multiplayer_server.enabled,
can_play_realms: self.privileges.multiplayer_realms.enabled,
use_filter: self.profanity_filter.profanity_filter,
}
}
}

View File

@ -10,6 +10,7 @@
*/
use anyhow::Result;
use log::debug;
use serde::{Deserialize, Serialize};
use structs::profile::Profile;
@ -26,13 +27,24 @@ pub struct Session {
impl Session {
pub async fn from_id(db: &Database, id: i64) -> Option<Self> {
let record = sqlx::query_as!(RawSession, "SELECT * FROM sessions WHERE id = $1", id)
let record = sqlx::query_as!(SessionRaw, "SELECT * FROM sessions WHERE id = $1", id)
.fetch_one(&db.pool)
.await;
match record {
Ok(r) => Some(r.complete(db).await),
Err(_) => None,
Err(e) => { debug!("{e}"); None },
}
}
pub async fn from_profile(db: &Database, profile: &Profile) -> Option<Self> {
let record = sqlx::query_as!(SessionRaw, "SELECT * FROM sessions WHERE profile = $1", profile.id)
.fetch_one(&db.pool)
.await;
match record {
Ok(r) => Some(r.complete(db).await),
Err(e) => { debug!("{e}"); None },
}
}
@ -43,17 +55,25 @@ impl Session {
Ok(())
}
pub async fn delete(&self, db: &Database) -> Result<()> {
sqlx::query!("DELETE FROM sessions WHERE id = $1", self.id)
.execute(&db.pool)
.await?;
Ok(())
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct RawSession {
pub struct SessionRaw {
pub id: i64,
pub profile: i64,
pub server_id: String,
pub ip_addr: String
}
impl RawSession {
impl SessionRaw {
pub async fn complete(self, db: &Database) -> Session {
Session {
id: self.id,

View File

@ -9,10 +9,10 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::time::UNIX_EPOCH;
use serde::{Deserialize, Serialize};
use json::{object, JsonValue};
use serde_json::Value;
use tide::prelude::json;
use structs::profile::Profile;
use crate::*;
@ -21,19 +21,19 @@ use crate::*;
pub struct TexturedObject {}
impl TexturedObject {
pub async fn from_profile(db: &Database, profile: &Profile) -> JsonValue {
let mut object = object! {
timestamp: std::time::SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards?!").as_millis() as u64,
profile_id: profile.uuid.to_owned(),
profile_name: profile.name.to_owned(),
textures: object!{}
};
pub async fn from_profile(db: &Database, profile: &Profile) -> Value {
let mut object = json!({
"timestamp": get_unix_timestamp() as u64,
"profile_id": profile.uuid.to_owned(),
"profile_name": profile.name.to_owned(),
"textures": {}
});
if profile.skin_variant != "NONE" {
let skin_url = profile.get_skin(db).await;
if skin_url.is_some() {
object["textures"]["SKIN"] = object! { url: skin_url };
object["textures"]["SKIN"] = json!({ "url": skin_url });
}
}
@ -41,36 +41,36 @@ impl TexturedObject {
let cape_url = profile.get_cape(db).await;
if cape_url.is_some() {
object["textures"]["CAPE"] = object! { url: cape_url };
object["textures"]["CAPE"] = json!({ "url": cape_url });
}
}
object! {
id: profile.uuid.replace("-", ""),
name: profile.name.to_owned(),
properties: [
json!({
"id": profile.uuid.replace("-", ""),
"name": profile.name.to_owned(),
"properties": [
// TODO: signing textures
// unsigned ? encode : sign
Self::encode_textures(&object)
// Self::sign_textures(object)
]
}
})
}
pub fn encode_textures(textures: &JsonValue) -> JsonValue {
pub fn encode_textures(textures: &Value) -> Value {
use base64::{Engine, engine::general_purpose::URL_SAFE as base64};
let serialized = textures.to_string();
let mut encoded = String::new();
base64.encode_string(serialized, &mut encoded);
object! {
name: "textures",
value: encoded
}
json!({
"name": "textures",
"value": encoded
})
}
pub fn sign_textures(textures: &JsonValue) -> JsonValue {
pub fn sign_textures(textures: &Value) -> Value {
// TODO: signing textures
unimplemented!()
}

View File

@ -10,6 +10,7 @@
*/
use anyhow::Result;
use log::debug;
use serde::{Deserialize, Serialize};
use sqlx::Error;
@ -29,35 +30,35 @@ pub struct Token {
impl Token {
pub async fn from_id(db: &Database, id: i64) -> Option<Self> {
let record = sqlx::query_as!(RawToken, "SELECT * FROM tokens WHERE id = $1", id)
let record = sqlx::query_as!(TokenRaw, "SELECT * FROM tokens WHERE id = $1", id)
.fetch_one(&db.pool)
.await;
match record {
Ok(r) => Some(r.complete(db).await),
Err(_) => None,
Err(e) => { debug!("{e}"); None },
}
}
pub async fn from_access_token(db: &Database, access: String) -> Option<Self> {
let record = sqlx::query_as!(RawToken, "SELECT * FROM tokens WHERE access = $1", access)
let record = sqlx::query_as!(TokenRaw, "SELECT * FROM tokens WHERE access = $1", access)
.fetch_one(&db.pool)
.await;
match record {
Ok(r) => Some(r.complete(db).await),
Err(_) => None
Err(e) => { debug!("{e}"); None },
}
}
pub async fn from_client_token(db: &Database, client: String) -> Option<Self> {
let record = sqlx::query_as!(RawToken, "SELECT * FROM tokens WHERE client = $1", client)
let record = sqlx::query_as!(TokenRaw, "SELECT * FROM tokens WHERE client = $1", client)
.fetch_one(&db.pool)
.await;
match record {
Ok(r) => Some(r.complete(db).await),
Err(_) => None
Err(e) => { debug!("{e}"); None },
}
}
@ -65,6 +66,55 @@ impl Token {
random_string::generate(128, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_.")
}
pub fn rehyphenate(uuid: String) -> String {
format!("{}-{}-{}-{}-{}",
uuid[0..8].to_string(),
uuid[8..12].to_string(),
uuid[12..16].to_string(),
uuid[16..20].to_string(),
uuid[20..32].to_string()
)
}
pub async fn new(db: &Database, account: Account, client_token: String) -> Option<Token> {
let access_token = Self::random_token();
let issued = (get_unix_timestamp() / 1000) as i64;
let expires = issued + 604800;
let record = sqlx::query!("INSERT INTO tokens(access, client, account, issued, expires) VALUES ($1, $2, $3, $4, $5) RETURNING *",
access_token, client_token, account.id, issued, expires)
.fetch_one(&db.pool)
.await;
match record {
Ok(r) => Some(Token {
id: r.id,
access: access_token,
client: client_token,
account,
issued,
expires,
}),
Err(e) => { debug!("{e}"); None },
}
}
pub async fn delete(&self, db: &Database) -> Result<()> {
sqlx::query!("DELETE FROM tokens WHERE id = $1", self.id)
.execute(&db.pool)
.await?;
Ok(())
}
pub async fn delete_all_from(db: &Database, account: Account) -> Result<()> {
sqlx::query!("DELETE FROM tokens WHERE account = $1", account.id)
.execute(&db.pool)
.await?;
Ok(())
}
async fn remove_expired(db: &Database) -> Result<()> {
let time = (get_unix_timestamp() / 1000) as f64;
sqlx::query!("DELETE FROM tokens WHERE expires <= $1", time)
@ -94,16 +144,16 @@ impl Token {
}
}
pub struct RawToken {
id: i64,
access: String,
client: String,
account: i64,
issued: i64,
expires: i64
pub struct TokenRaw {
pub id: i64,
pub access: String,
pub client: String,
pub account: i64,
pub issued: i64,
pub expires: i64
}
impl RawToken {
impl TokenRaw {
pub async fn complete(self, db: &Database) -> Token {
Token {
id: self.id,

View File

@ -9,11 +9,21 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use anyhow::anyhow;
use tide::{prelude::*, Request, Result};
use regex::Regex;
use yggdrasil::Database;
pub struct Validate {}
impl Validate {
pub fn email(e: &str) -> bool {
Regex::new(r"[a-z0-9_\-.]*@[a-z0-9.]*").unwrap().is_match(e)
}
pub fn lang(l: &str) -> bool {
Regex::new(r"[a-z]{2}-[a-z]{2}").unwrap().is_match(l)
}
pub fn country(c: &str) -> bool {
Regex::new(r"[A-Z]{2}").unwrap().is_match(c)
}
}
pub async fn validate(req: Request<Database>) -> Result {
Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
}

14
yggdrasil Executable file
View File

@ -0,0 +1,14 @@
#! /usr/bin/bash
#
# Yggdrasil: Minecraft authentication server
# Copyright (C) 2023 0xf8.dev@proton.me
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
#
DATABASE_URL="sqlite:yggdrasil.db" cargo run --bin yggdrasil -- "$@"