From 93176289351222564d507abb301ece7f7d320d32 Mon Sep 17 00:00:00 2001
From: 0xf8 <0xf8.dev@proton.me>
Date: Wed, 21 Jun 2023 04:13:14 -0400
Subject: [PATCH] First release. Basic authentication and profiles work great
---
Cargo.toml | 16 +++
dbtool | 14 +++
src/dbtool/add_account.rs | 52 ++++++++
src/dbtool/add_profile.rs | 53 +++++++++
src/dbtool/attach_profile.rs | 46 ++++++++
.../refresh.rs => dbtool/del_account.rs} | 31 ++++-
.../invalidate.rs => dbtool/del_profile.rs} | 31 ++++-
src/dbtool/dump.rs | 87 ++++++++++++++
src/dbtool/mod.rs | 76 ++++++++++++
src/dbtool/search.rs | 111 ++++++++++++++++++
src/main.rs | 14 ++-
src/main_dbtool.rs | 50 ++++++++
src/server/account/profiles.rs | 2 +-
src/server/account/skin.rs | 2 +-
.../{authserver => auth}/authenticate.rs | 43 ++++---
src/server/auth/invalidate.rs | 48 ++++++++
src/server/{authserver => auth}/mod.rs | 0
src/server/auth/refresh.rs | 84 +++++++++++++
src/server/auth/signout.rs | 48 ++++++++
src/server/auth/validate.rs | 46 ++++++++
src/server/authlib/mod.rs | 4 +-
src/server/authserver/signout.rs | 19 ---
src/server/minecraft/capes.rs | 3 +-
src/server/mod.rs | 27 +++--
src/server/session/has_joined.rs | 58 +++++++++
src/server/session/join.rs | 60 ++++++++++
src/server/{sessionserver => session}/mod.rs | 4 +-
src/server/session/profile.rs | 40 +++++++
src/server/sessionserver/has_joined.rs | 19 ---
src/server/sessionserver/join.rs | 19 ---
src/server/sessionserver/profile.rs | 19 ---
src/util/database.rs | 14 +++
src/util/errors.rs | 14 ++-
src/util/input.rs | 58 +++++++++
src/util/mod.rs | 17 ++-
src/util/structs/account.rs | 42 ++++++-
src/util/structs/blocked_server.rs | 3 +-
src/util/structs/cape.rs | 3 +-
src/util/structs/profile.rs | 78 ++++++++++--
src/util/structs/profile_attributes.rs | 62 +++++++++-
src/util/structs/session.rs | 28 ++++-
src/util/structs/textured_object.rs | 46 ++++----
src/util/structs/token.rs | 78 +++++++++---
src/{server/authserver => util}/validate.rs | 22 +++-
yggdrasil | 14 +++
45 files changed, 1410 insertions(+), 195 deletions(-)
create mode 100755 dbtool
create mode 100644 src/dbtool/add_account.rs
create mode 100644 src/dbtool/add_profile.rs
create mode 100644 src/dbtool/attach_profile.rs
rename src/{server/authserver/refresh.rs => dbtool/del_account.rs} (55%)
rename src/{server/authserver/invalidate.rs => dbtool/del_profile.rs} (55%)
create mode 100644 src/dbtool/dump.rs
create mode 100644 src/dbtool/mod.rs
create mode 100644 src/dbtool/search.rs
create mode 100644 src/main_dbtool.rs
rename src/server/{authserver => auth}/authenticate.rs (66%)
create mode 100644 src/server/auth/invalidate.rs
rename src/server/{authserver => auth}/mod.rs (100%)
create mode 100644 src/server/auth/refresh.rs
create mode 100644 src/server/auth/signout.rs
create mode 100644 src/server/auth/validate.rs
delete mode 100644 src/server/authserver/signout.rs
create mode 100644 src/server/session/has_joined.rs
create mode 100644 src/server/session/join.rs
rename src/server/{sessionserver => session}/mod.rs (90%)
create mode 100644 src/server/session/profile.rs
delete mode 100644 src/server/sessionserver/has_joined.rs
delete mode 100644 src/server/sessionserver/join.rs
delete mode 100644 src/server/sessionserver/profile.rs
create mode 100644 src/util/input.rs
rename src/{server/authserver => util}/validate.rs (65%)
create mode 100755 yggdrasil
diff --git a/Cargo.toml b/Cargo.toml
index 5aab1b9..f6b5ca3 100644
--- a/Cargo.toml
+++ b/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"
diff --git a/dbtool b/dbtool
new file mode 100755
index 0000000..f813ac4
--- /dev/null
+++ b/dbtool
@@ -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 .
+#
+
+DATABASE_URL="sqlite:yggdrasil.db" cargo run --bin dbtool -- "$@"
\ No newline at end of file
diff --git a/src/dbtool/add_account.rs b/src/dbtool/add_account.rs
new file mode 100644
index 0000000..2695657
--- /dev/null
+++ b/src/dbtool/add_account.rs
@@ -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 .
+ */
+
+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 ") };
+
+ // 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(())
+ }
+}
+
diff --git a/src/dbtool/add_profile.rs b/src/dbtool/add_profile.rs
new file mode 100644
index 0000000..37e3731
--- /dev/null
+++ b/src/dbtool/add_profile.rs
@@ -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 .
+ */
+
+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 ") }
+
+ // 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(())
+ }
+}
+
diff --git a/src/dbtool/attach_profile.rs b/src/dbtool/attach_profile.rs
new file mode 100644
index 0000000..8008ca1
--- /dev/null
+++ b/src/dbtool/attach_profile.rs
@@ -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 .
+ */
+
+
+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 ") }
+
+ // 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
+ }
+}
diff --git a/src/server/authserver/refresh.rs b/src/dbtool/del_account.rs
similarity index 55%
rename from src/server/authserver/refresh.rs
rename to src/dbtool/del_account.rs
index 568298b..1fe1135 100644
--- a/src/server/authserver/refresh.rs
+++ b/src/dbtool/del_account.rs
@@ -9,11 +9,30 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
-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) -> Result {
- Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
-}
\ No newline at end of file
+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 ") }
+
+ // 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(())
+ }
+}
diff --git a/src/server/authserver/invalidate.rs b/src/dbtool/del_profile.rs
similarity index 55%
rename from src/server/authserver/invalidate.rs
rename to src/dbtool/del_profile.rs
index e842e0f..59c32ca 100644
--- a/src/server/authserver/invalidate.rs
+++ b/src/dbtool/del_profile.rs
@@ -9,11 +9,30 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
-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) -> Result {
- Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
-}
\ No newline at end of file
+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 ") }
+
+ // 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(())
+ }
+}
diff --git a/src/dbtool/dump.rs b/src/dbtool/dump.rs
new file mode 100644
index 0000000..3d7edcc
--- /dev/null
+++ b/src/dbtool/dump.rs
@@ -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 .
+ */
+
+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 ") }
+
+ 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(())
+ }
+}
+
diff --git a/src/dbtool/mod.rs b/src/dbtool/mod.rs
new file mode 100644
index 0000000..8814d01
--- /dev/null
+++ b/src/dbtool/mod.rs
@@ -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 .
+ */
+
+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,
+}
+
+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(())
+}
\ No newline at end of file
diff --git a/src/dbtool/search.rs b/src/dbtool/search.rs
new file mode 100644
index 0000000..036568d
--- /dev/null
+++ b/src/dbtool/search.rs
@@ -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 .
+ */
+
+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) -> 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) -> 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) -> 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) -> 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) -> 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 [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(())
+ }
+}
+
diff --git a/src/main.rs b/src/main.rs
index d73ef12..55ac23e 100644
--- a/src/main.rs
+++ b/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!");
diff --git a/src/main_dbtool.rs b/src/main_dbtool.rs
new file mode 100644
index 0000000..48cf605
--- /dev/null
+++ b/src/main_dbtool.rs
@@ -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 .
+ */
+
+#![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())
+}
diff --git a/src/server/account/profiles.rs b/src/server/account/profiles.rs
index 11944e3..f0f7a31 100644
--- a/src/server/account/profiles.rs
+++ b/src/server/account/profiles.rs
@@ -10,7 +10,7 @@
*/
use anyhow::anyhow;
-use tide::{prelude::*, Request, Result};
+use tide::{Request, Result};
use yggdrasil::Database;
diff --git a/src/server/account/skin.rs b/src/server/account/skin.rs
index 1af092f..a4bac06 100644
--- a/src/server/account/skin.rs
+++ b/src/server/account/skin.rs
@@ -10,7 +10,7 @@
*/
use anyhow::anyhow;
-use tide::{prelude::*, Request, Result};
+use tide::{Request, Result};
use yggdrasil::Database;
diff --git a/src/server/authserver/authenticate.rs b/src/server/auth/authenticate.rs
similarity index 66%
rename from src/server/authserver/authenticate.rs
rename to src/server/auth/authenticate.rs
index 137c1d6..a544144 100644
--- a/src/server/authserver/authenticate.rs
+++ b/src/server/auth/authenticate.rs
@@ -9,32 +9,35 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
+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,
+
#[serde(rename = "requestUser")]
- pub request_user: Option
+ pub request_user: Option,
}
pub async fn authenticate(mut req: Request) -> Result {
let Ok(body) = req.body_json::().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) -> 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) -> 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) -> 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()
});
}
diff --git a/src/server/auth/invalidate.rs b/src/server/auth/invalidate.rs
new file mode 100644
index 0000000..ae3410e
--- /dev/null
+++ b/src/server/auth/invalidate.rs
@@ -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 .
+ */
+
+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) -> Result {
+ let Ok(body) = req.body_json::().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())
+}
\ No newline at end of file
diff --git a/src/server/authserver/mod.rs b/src/server/auth/mod.rs
similarity index 100%
rename from src/server/authserver/mod.rs
rename to src/server/auth/mod.rs
diff --git a/src/server/auth/refresh.rs b/src/server/auth/refresh.rs
new file mode 100644
index 0000000..55581ba
--- /dev/null
+++ b/src/server/auth/refresh.rs
@@ -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 .
+ */
+
+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,
+}
+
+pub async fn refresh(mut req: Request) -> Result {
+ let Ok(body) = req.body_json::().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())
+}
\ No newline at end of file
diff --git a/src/server/auth/signout.rs b/src/server/auth/signout.rs
new file mode 100644
index 0000000..545a26c
--- /dev/null
+++ b/src/server/auth/signout.rs
@@ -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 .
+ */
+
+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) -> Result {
+ let Ok(body) = req.body_json::().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())
+}
\ No newline at end of file
diff --git a/src/server/auth/validate.rs b/src/server/auth/validate.rs
new file mode 100644
index 0000000..ce10a69
--- /dev/null
+++ b/src/server/auth/validate.rs
@@ -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 .
+ */
+
+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) -> Result {
+ let Ok(body) = req.body_json::().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())
+}
\ No newline at end of file
diff --git a/src/server/authlib/mod.rs b/src/server/authlib/mod.rs
index 1bc9e27..04a460a 100644
--- a/src/server/authlib/mod.rs
+++ b/src/server/authlib/mod.rs
@@ -20,8 +20,8 @@ pub fn nest(db: Database) -> tide::Server {
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
}
diff --git a/src/server/authserver/signout.rs b/src/server/authserver/signout.rs
deleted file mode 100644
index 0767360..0000000
--- a/src/server/authserver/signout.rs
+++ /dev/null
@@ -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 .
- */
-
-use anyhow::anyhow;
-use tide::{prelude::*, Request, Result};
-
-use yggdrasil::Database;
-
-pub async fn signout(req: Request) -> Result {
- Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
-}
\ No newline at end of file
diff --git a/src/server/minecraft/capes.rs b/src/server/minecraft/capes.rs
index 1d3fc41..83623f4 100644
--- a/src/server/minecraft/capes.rs
+++ b/src/server/minecraft/capes.rs
@@ -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) -> Result {
Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
}
-
pub async fn delete_cape(req: Request) -> Result {
Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
}
\ No newline at end of file
diff --git a/src/server/mod.rs b/src/server/mod.rs
index ef63c33..1b19247 100644
--- a/src/server/mod.rs
+++ b/src/server/mod.rs
@@ -9,25 +9,29 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
-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::() {
+ 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| async move {
- req.append_header("x-authlib-injector-api-location", "/authlib/");
- Ok("Yggdrasil")
+ app.at("/").get(|req: Request| 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?;
diff --git a/src/server/session/has_joined.rs b/src/server/session/has_joined.rs
new file mode 100644
index 0000000..eb732d7
--- /dev/null
+++ b/src/server/session/has_joined.rs
@@ -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 .
+ */
+
+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,
+}
+
+pub async fn has_joined(mut req: Request) -> Result {
+ let Ok(body) = req.body_json::().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())
+}
\ No newline at end of file
diff --git a/src/server/session/join.rs b/src/server/session/join.rs
new file mode 100644
index 0000000..b208cf0
--- /dev/null
+++ b/src/server/session/join.rs
@@ -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 .
+ */
+
+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) -> Result {
+ let Ok(body) = req.body_json::().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())
+}
+
diff --git a/src/server/sessionserver/mod.rs b/src/server/session/mod.rs
similarity index 90%
rename from src/server/sessionserver/mod.rs
rename to src/server/session/mod.rs
index db45c96..a622ba5 100644
--- a/src/server/sessionserver/mod.rs
+++ b/src/server/session/mod.rs
@@ -22,8 +22,8 @@ pub fn nest(db: Database) -> tide::Server {
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
diff --git a/src/server/session/profile.rs b/src/server/session/profile.rs
new file mode 100644
index 0000000..93112d7
--- /dev/null
+++ b/src/server/session/profile.rs
@@ -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 .
+ */
+
+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) -> 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())
+}
\ No newline at end of file
diff --git a/src/server/sessionserver/has_joined.rs b/src/server/sessionserver/has_joined.rs
deleted file mode 100644
index b3119f0..0000000
--- a/src/server/sessionserver/has_joined.rs
+++ /dev/null
@@ -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 .
- */
-
-use anyhow::anyhow;
-use tide::{prelude::*, Request, Result};
-
-use yggdrasil::Database;
-
-pub async fn has_joined(req: Request) -> Result {
- Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
-}
\ No newline at end of file
diff --git a/src/server/sessionserver/join.rs b/src/server/sessionserver/join.rs
deleted file mode 100644
index ceffb1e..0000000
--- a/src/server/sessionserver/join.rs
+++ /dev/null
@@ -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 .
- */
-
-use anyhow::anyhow;
-use tide::{prelude::*, Request, Result};
-
-use yggdrasil::Database;
-
-pub async fn join(req: Request) -> Result {
- Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
-}
\ No newline at end of file
diff --git a/src/server/sessionserver/profile.rs b/src/server/sessionserver/profile.rs
deleted file mode 100644
index 3c8fdff..0000000
--- a/src/server/sessionserver/profile.rs
+++ /dev/null
@@ -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 .
- */
-
-use anyhow::anyhow;
-use tide::{prelude::*, Request, Result};
-
-use yggdrasil::Database;
-
-pub async fn profile(req: Request) -> Result {
- Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
-}
\ No newline at end of file
diff --git a/src/util/database.rs b/src/util/database.rs
index 31e64f9..676f73e 100644
--- a/src/util/database.rs
+++ b/src/util/database.rs
@@ -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());
+ }
+}
+
diff --git a/src/util/errors.rs b/src/util/errors.rs
index 3bb6ea7..dba1d55 100644
--- a/src/util/errors.rs
+++ b/src/util/errors.rs
@@ -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
}
}
-
}
diff --git a/src/util/input.rs b/src/util/input.rs
new file mode 100644
index 0000000..12a8aeb
--- /dev/null
+++ b/src/util/input.rs
@@ -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 .
+ */
+
+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 {
+ let theme = ColorfulTheme::default();
+
+ let mut password = Password::with_theme(&theme);
+ password.with_prompt("Password");
+
+ Ok(password.interact()?)
+ }
+
+ pub async fn attributes() -> Result {
+ 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)
+ }
+}
+
diff --git a/src/util/mod.rs b/src/util/mod.rs
index 7abc52c..1918a27 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -9,14 +9,25 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
+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()
+}
\ No newline at end of file
diff --git a/src/util/structs/account.rs b/src/util/structs/account.rs
index 3de6199..d231ee0 100644
--- a/src/util/structs/account.rs
+++ b/src/util/structs/account.rs
@@ -9,6 +9,8 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
+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 {
+ 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 {
+ 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,
diff --git a/src/util/structs/blocked_server.rs b/src/util/structs/blocked_server.rs
index c9854d1..2eeca1e 100644
--- a/src/util/structs/blocked_server.rs
+++ b/src/util/structs/blocked_server.rs
@@ -9,6 +9,7 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
+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 },
}
}
}
\ No newline at end of file
diff --git a/src/util/structs/cape.rs b/src/util/structs/cape.rs
index 53bce8e..78ffd65 100644
--- a/src/util/structs/cape.rs
+++ b/src/util/structs/cape.rs
@@ -9,6 +9,7 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
+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 },
}
}
diff --git a/src/util/structs/profile.rs b/src/util/structs/profile.rs
index d70b832..56fa5ab 100644
--- a/src/util/structs/profile.rs
+++ b/src/util/structs/profile.rs
@@ -9,12 +9,16 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
+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>,
pub active_cape: Option,
- 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::from_id(db, self.owner).await
+ }
+
+ pub async fn new(db: &Database, owner: Account, name: String, attr: ProfileAttributesSimple) -> Result {
+ 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 {
+ 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,
+ 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::(self.attributes.as_str())
+ .expect("Couldn't parse profile attributes").to_simple(),
}
}
}
\ No newline at end of file
diff --git a/src/util/structs/profile_attributes.rs b/src/util/structs/profile_attributes.rs
index 755ef9a..5f2615a 100644
--- a/src/util/structs/profile_attributes.rs
+++ b/src/util/structs/profile_attributes.rs
@@ -9,19 +9,52 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
-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,
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/util/structs/session.rs b/src/util/structs/session.rs
index 94e5030..441aff8 100644
--- a/src/util/structs/session.rs
+++ b/src/util/structs/session.rs
@@ -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 {
- 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 {
+ 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,
diff --git a/src/util/structs/textured_object.rs b/src/util/structs/textured_object.rs
index 706633b..dd42f5b 100644
--- a/src/util/structs/textured_object.rs
+++ b/src/util/structs/textured_object.rs
@@ -9,10 +9,10 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
-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!()
}
diff --git a/src/util/structs/token.rs b/src/util/structs/token.rs
index 06dd0c5..4098749 100644
--- a/src/util/structs/token.rs
+++ b/src/util/structs/token.rs
@@ -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 {
- 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 {
- 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 {
- 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 {
+ 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,
diff --git a/src/server/authserver/validate.rs b/src/util/validate.rs
similarity index 65%
rename from src/server/authserver/validate.rs
rename to src/util/validate.rs
index e4bca3e..8a9b6b8 100644
--- a/src/server/authserver/validate.rs
+++ b/src/util/validate.rs
@@ -9,11 +9,21 @@
* You should have received a copy of the GNU General Public License along with this program. If not, see .
*/
-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) -> Result {
- Err(tide::Error::new(501, anyhow!("Not implemented yet")).into())
-}
\ No newline at end of file
diff --git a/yggdrasil b/yggdrasil
new file mode 100755
index 0000000..7d785d1
--- /dev/null
+++ b/yggdrasil
@@ -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 .
+#
+
+DATABASE_URL="sqlite:yggdrasil.db" cargo run --bin yggdrasil -- "$@"
\ No newline at end of file