First release. Basic authentication and profiles work great
This commit is contained in:
parent
5b1a0966f6
commit
9317628935
16
Cargo.toml
16
Cargo.toml
@ -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
14
dbtool
Executable 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
52
src/dbtool/add_account.rs
Normal 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
53
src/dbtool/add_profile.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
46
src/dbtool/attach_profile.rs
Normal file
46
src/dbtool/attach_profile.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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(())
|
||||
}
|
||||
}
|
@ -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
87
src/dbtool/dump.rs
Normal 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
76
src/dbtool/mod.rs
Normal 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
111
src/dbtool/search.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
|
14
src/main.rs
14
src/main.rs
@ -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
50
src/main_dbtool.rs
Normal 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())
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
use anyhow::anyhow;
|
||||
use tide::{prelude::*, Request, Result};
|
||||
use tide::{Request, Result};
|
||||
|
||||
use yggdrasil::Database;
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
*/
|
||||
|
||||
use anyhow::anyhow;
|
||||
use tide::{prelude::*, Request, Result};
|
||||
use tide::{Request, Result};
|
||||
|
||||
use yggdrasil::Database;
|
||||
|
||||
|
@ -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()
|
||||
});
|
||||
}
|
48
src/server/auth/invalidate.rs
Normal file
48
src/server/auth/invalidate.rs
Normal 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())
|
||||
}
|
84
src/server/auth/refresh.rs
Normal file
84
src/server/auth/refresh.rs
Normal 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())
|
||||
}
|
48
src/server/auth/signout.rs
Normal file
48
src/server/auth/signout.rs
Normal 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())
|
||||
}
|
46
src/server/auth/validate.rs
Normal file
46
src/server/auth/validate.rs
Normal 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())
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
@ -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())
|
||||
}
|
@ -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?;
|
||||
|
58
src/server/session/has_joined.rs
Normal file
58
src/server/session/has_joined.rs
Normal 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())
|
||||
}
|
60
src/server/session/join.rs
Normal file
60
src/server/session/join.rs
Normal 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())
|
||||
}
|
||||
|
@ -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
|
40
src/server/session/profile.rs
Normal file
40
src/server/session/profile.rs
Normal 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())
|
||||
}
|
@ -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())
|
||||
}
|
@ -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())
|
||||
}
|
@ -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())
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
58
src/util/input.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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,
|
||||
|
@ -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 },
|
||||
}
|
||||
}
|
||||
}
|
@ -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 },
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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!()
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {}
|
||||
|
||||
pub async fn validate(req: Request<Database>) -> Result {
|
||||
Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
14
yggdrasil
Executable file
14
yggdrasil
Executable 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 -- "$@"
|
Loading…
Reference in New Issue
Block a user