From 50c849282a21a7b41d1ba7b1d59ef82fe203c817 Mon Sep 17 00:00:00 2001 From: 0xf8 <0xf8.dev@proton.me> Date: Tue, 20 Jun 2023 00:52:08 -0400 Subject: [PATCH] Create stubs for a lot of routes; rework structs.rs to module --- Cargo.toml | 6 +- migrations/0_yggdrasil.sql | 12 +- src/main.rs | 8 +- src/server/account/mod.rs | 7 + src/server/account/profiles.rs | 21 ++ src/server/account/skin.rs | 27 ++ src/server/authlib/mod.rs | 6 +- src/server/authserver/authenticate.rs | 92 +++++ src/server/authserver/invalidate.rs | 19 + src/server/{session => authserver}/mod.rs | 11 + src/server/authserver/refresh.rs | 19 + src/server/authserver/signout.rs | 19 + src/server/authserver/validate.rs | 19 + src/server/minecraft/capes.rs | 24 ++ src/server/{auth => minecraft}/mod.rs | 4 + src/server/mod.rs | 17 +- src/server/sessionserver/has_joined.rs | 19 + src/server/sessionserver/join.rs | 19 + src/server/{services => sessionserver}/mod.rs | 7 + src/server/sessionserver/profile.rs | 19 + src/util/database.rs | 13 +- src/util/errors.rs | 39 ++ src/util/structs.rs | 333 ------------------ src/util/structs/Account.rs | 109 ++++++ src/util/structs/BlockedServer.rs | 35 ++ src/util/structs/Cape.rs | 38 ++ src/util/structs/Profile.rs | 144 ++++++++ src/util/structs/ProfileAttributes.rs | 38 ++ src/util/structs/Session.rs | 57 +++ src/util/structs/TexturedObject.rs | 77 ++++ src/util/structs/Token.rs | 64 ++++ src/util/structs/mod.rs | 21 ++ 32 files changed, 985 insertions(+), 358 deletions(-) create mode 100644 src/server/account/profiles.rs create mode 100644 src/server/account/skin.rs create mode 100644 src/server/authserver/authenticate.rs create mode 100644 src/server/authserver/invalidate.rs rename src/server/{session => authserver}/mod.rs (74%) create mode 100644 src/server/authserver/refresh.rs create mode 100644 src/server/authserver/signout.rs create mode 100644 src/server/authserver/validate.rs create mode 100644 src/server/minecraft/capes.rs rename src/server/{auth => minecraft}/mod.rs (87%) create mode 100644 src/server/sessionserver/has_joined.rs create mode 100644 src/server/sessionserver/join.rs rename src/server/{services => sessionserver}/mod.rs (84%) create mode 100644 src/server/sessionserver/profile.rs delete mode 100644 src/util/structs.rs create mode 100644 src/util/structs/Account.rs create mode 100644 src/util/structs/BlockedServer.rs create mode 100644 src/util/structs/Cape.rs create mode 100644 src/util/structs/Profile.rs create mode 100644 src/util/structs/ProfileAttributes.rs create mode 100644 src/util/structs/Session.rs create mode 100644 src/util/structs/TexturedObject.rs create mode 100644 src/util/structs/Token.rs create mode 100644 src/util/structs/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 2357303..5aab1b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,19 +9,21 @@ license = "GPL3.0-or-later" [dependencies] anyhow = "1.0.71" +async-std = { version = "1.12.0", features = ["attributes"] } base64 = "0.21.2" +bcrypt = "0.14.0" colored = "2.0.0" driftwood = "0.0.7" femme = "2.2.1" json = "0.12.4" log = "0.4.19" rand = "0.8.5" +random-string = "1.0.0" rsa = "0.9.2" serde = { version = "1.0.164", features = ["derive"] } serde_json = "1.0.97" sha2 = "0.10.7" -sqlx = { version = "0.6.3", features = ["sqlite", "runtime-tokio-native-tls"] } +sqlx = { version = "0.6.3", features = ["sqlite", "runtime-async-std-native-tls"] } tide = "0.16.0" time = "0.3.22" -tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } toml = "0.7.4" diff --git a/migrations/0_yggdrasil.sql b/migrations/0_yggdrasil.sql index 07881bb..9c3ede5 100644 --- a/migrations/0_yggdrasil.sql +++ b/migrations/0_yggdrasil.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS accounts ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, language TEXT NOT NULL, @@ -9,20 +9,20 @@ CREATE TABLE IF NOT EXISTS accounts ( ); CREATE TABLE IF NOT EXISTS blocked_servers ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY NOT NULL UNIQUE, pattern TEXT NOT NULL UNIQUE, sha1 TEXT NOT NULL UNIQUE, reason TEXT ); CREATE TABLE IF NOT EXISTS capes ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY NOT NULL UNIQUE, friendly_id TEXT NOT NULL UNIQUE, alias TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS profiles ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY NOT NULL UNIQUE, uuid TEXT NOT NULL UNIQUE, created INTEGER NOT NULL, owner INTEGER NOT NULL, @@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS profiles ( ); CREATE TABLE IF NOT EXISTS sessions ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY NOT NULL UNIQUE, profile INTEGER NOT NULL, server_id TEXT NOT NULL, ip_addr TEXT NOT NULL, @@ -45,7 +45,7 @@ CREATE TABLE IF NOT EXISTS sessions ( ); CREATE TABLE IF NOT EXISTS tokens ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY NOT NULL UNIQUE, access TEXT NOT NULL UNIQUE, client TEXT NOT NULL, account INTEGER NOT NULL, diff --git a/src/main.rs b/src/main.rs index 5b3e4ab..d73ef12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,14 +13,12 @@ use anyhow::{bail, Result}; use log::{debug, error, info, log, trace, warn}; -use tokio::spawn; use yggdrasil::*; mod server; - -#[tokio::main] +#[async_std::main] async fn main() -> Result<()> { // Early catch if std::env::var("DATABASE_URL").is_err() { @@ -39,8 +37,8 @@ async fn main() -> Result<()> { info!("Database URL: {}", std::env::var("DATABASE_URL")?); // Start server - let server_thread = spawn(server::start(db)); - server_thread.await??; + let server_thread = async_std::task::spawn(server::start(db)); + server_thread.await?; warn!("Server stopped!"); diff --git a/src/server/account/mod.rs b/src/server/account/mod.rs index fe75014..0bf65ab 100644 --- a/src/server/account/mod.rs +++ b/src/server/account/mod.rs @@ -14,10 +14,17 @@ pub use tide::{Middleware, prelude::*, Request, Response, Result, utils::After}; pub use yggdrasil::*; +mod profiles; +mod skin; + pub fn nest(db: Database) -> tide::Server { info!("Loading nest"); let mut nest = tide::with_state(db); + nest.at("profiles/minecraft").post(profiles::profiles); + nest.at("profile/skins").put(skin::put_skin); + nest.at("profile/active").delete(skin::delete_skin); + nest } diff --git a/src/server/account/profiles.rs b/src/server/account/profiles.rs new file mode 100644 index 0000000..11944e3 --- /dev/null +++ b/src/server/account/profiles.rs @@ -0,0 +1,21 @@ +/* + * 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 profiles(req: Request) -> Result { + + + Err(tide::Error::new(501, anyhow!("Not implemented yet")).into()) +} \ No newline at end of file diff --git a/src/server/account/skin.rs b/src/server/account/skin.rs new file mode 100644 index 0000000..1af092f --- /dev/null +++ b/src/server/account/skin.rs @@ -0,0 +1,27 @@ +/* + * 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 delete_skin(req: Request) -> Result { + // let authorization = req.header("authorization"); + // if (authorization.is_none()) { return Err(YggdrasilError::new_base().into()) } + + Err(tide::Error::new(501, anyhow!("Not implemented yet")).into()) +} + +// TODO: file uploading +pub async fn put_skin(req: Request) -> Result { + Err(tide::Error::new(501, anyhow!("Not implemented yet")).into()) +} \ No newline at end of file diff --git a/src/server/authlib/mod.rs b/src/server/authlib/mod.rs index 0a88a84..1bc9e27 100644 --- a/src/server/authlib/mod.rs +++ b/src/server/authlib/mod.rs @@ -17,9 +17,11 @@ pub use yggdrasil::*; pub fn nest(db: Database) -> tide::Server { info!("Loading nest"); - let mut nest = tide::with_state(db); + 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 } @@ -39,4 +41,4 @@ async fn authlib_meta(req: Request) -> Result { "skinDomains": config.skin_domains // TODO: public key signature }).into()) -} \ No newline at end of file +} diff --git a/src/server/authserver/authenticate.rs b/src/server/authserver/authenticate.rs new file mode 100644 index 0000000..137c1d6 --- /dev/null +++ b/src/server/authserver/authenticate.rs @@ -0,0 +1,92 @@ +/* + * 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::*; +use yggdrasil::errors::YggdrasilError; +use yggdrasil::structs::{Account::Account, Cape::Cape, Token::Token}; + +#[derive(Deserialize, Debug)] +struct Agent { + pub name: String, + pub version: i64 +} + +#[derive(Deserialize, Debug)] +struct AuthenticateBody { + pub agent: Agent, + pub username: String, + pub password: String, // hashed? + #[serde(rename = "clientToken")] + pub client_token: Option, + #[serde(rename = "requestUser")] + 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()); + }; + + // Check current agent + if body.agent.name != "Minecraft" || body.agent.version != 1 { + return Err(YggdrasilError::new_bad_request("Unsupported game.").into()); + }; + + // 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()) + }; + + // Password incorrect + if account.password_hash != body.password { + return Err(YggdrasilError::new_forbidden("Invalid credentials. Invalid username or password.").into()); + } + + // Response + let client_token = match body.client_token { + None => Token::random_token(), + Some(t) => t + }; + + let mut response = json!({ + "clientToken": client_token, + "accessToken": "", // TODO: register_token + "availableProfiles": [], // TODO: get account profiles + }); + + // Give selected profile if it exists + if account.selected_profile.is_some() { + let profile = account.to_owned().selected_profile.unwrap(); + + response["selectedProfile"] = json!({ + "uuid": 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.unwrap(), + "attributes": profile.attributes.to_json() + }); + } + + // Give user if requested + if body.request_user.unwrap_or(false) { response["user"] = account.to_user() } + + Ok(response.into()) +} \ No newline at end of file diff --git a/src/server/authserver/invalidate.rs b/src/server/authserver/invalidate.rs new file mode 100644 index 0000000..e842e0f --- /dev/null +++ b/src/server/authserver/invalidate.rs @@ -0,0 +1,19 @@ +/* + * 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 invalidate(req: Request) -> Result { + Err(tide::Error::new(501, anyhow!("Not implemented yet")).into()) +} \ No newline at end of file diff --git a/src/server/session/mod.rs b/src/server/authserver/mod.rs similarity index 74% rename from src/server/session/mod.rs rename to src/server/authserver/mod.rs index fe75014..e57d6c3 100644 --- a/src/server/session/mod.rs +++ b/src/server/authserver/mod.rs @@ -14,10 +14,21 @@ pub use tide::{Middleware, prelude::*, Request, Response, Result, utils::After}; pub use yggdrasil::*; +mod authenticate; +mod invalidate; +mod refresh; +mod signout; +mod validate; + pub fn nest(db: Database) -> tide::Server { info!("Loading nest"); let mut nest = tide::with_state(db); + nest.at("authenticate").post(authenticate::authenticate); + nest.at("invalidate").post(invalidate::invalidate); + nest.at("refresh").post(refresh::refresh); + nest.at("signout").post(signout::signout); + nest.at("validate").post(validate::validate); nest } diff --git a/src/server/authserver/refresh.rs b/src/server/authserver/refresh.rs new file mode 100644 index 0000000..568298b --- /dev/null +++ b/src/server/authserver/refresh.rs @@ -0,0 +1,19 @@ +/* + * 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 refresh(req: Request) -> Result { + Err(tide::Error::new(501, anyhow!("Not implemented yet")).into()) +} \ No newline at end of file diff --git a/src/server/authserver/signout.rs b/src/server/authserver/signout.rs new file mode 100644 index 0000000..0767360 --- /dev/null +++ b/src/server/authserver/signout.rs @@ -0,0 +1,19 @@ +/* + * 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/authserver/validate.rs b/src/server/authserver/validate.rs new file mode 100644 index 0000000..e4bca3e --- /dev/null +++ b/src/server/authserver/validate.rs @@ -0,0 +1,19 @@ +/* + * 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 validate(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 new file mode 100644 index 0000000..1d3fc41 --- /dev/null +++ b/src/server/minecraft/capes.rs @@ -0,0 +1,24 @@ +/* + * 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 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/auth/mod.rs b/src/server/minecraft/mod.rs similarity index 87% rename from src/server/auth/mod.rs rename to src/server/minecraft/mod.rs index fe75014..35acf8a 100644 --- a/src/server/auth/mod.rs +++ b/src/server/minecraft/mod.rs @@ -14,10 +14,14 @@ pub use tide::{Middleware, prelude::*, Request, Response, Result, utils::After}; pub use yggdrasil::*; +mod capes; + pub fn nest(db: Database) -> tide::Server { info!("Loading nest"); let mut nest = tide::with_state(db); + nest.at("/profiles/capes/active").put(capes::upload_cape); + nest.at("/profiles/capes/active").delete(capes::delete_cape); nest } diff --git a/src/server/mod.rs b/src/server/mod.rs index 4b3b7bc..ef63c33 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -9,15 +9,16 @@ * You should have received a copy of the GNU General Public License along with this program. If not, see . */ +use log::info; use tide::{Request, Response, utils::After}; use yggdrasil::*; mod account; -mod auth; +mod authserver; mod authlib; -mod services; -mod session; +mod minecraft; +mod sessionserver; pub async fn start(db: Database) -> anyhow::Result<()> { let mut app = tide::with_state(db.to_owned()); @@ -31,6 +32,8 @@ pub async fn start(db: Database) -> anyhow::Result<()> { // TODO: pass through // err.3: bool + } else if let Some(err) = res.error() { + res.set_body(format!("{}\n", err)); } Ok(res) @@ -43,11 +46,11 @@ pub async fn start(db: Database) -> anyhow::Result<()> { }); // Routes - app.at("/account/").nest(account::nest(db.to_owned())); - app.at("/auth/").nest(auth::nest(db.to_owned())); - app.at("/services/").nest(services::nest(db.to_owned())); - app.at("/session/").nest(session::nest(db.to_owned())); 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())); // Listen app.listen(format!("{}:{}", &db.config.address, &db.config.port)).await?; diff --git a/src/server/sessionserver/has_joined.rs b/src/server/sessionserver/has_joined.rs new file mode 100644 index 0000000..b3119f0 --- /dev/null +++ b/src/server/sessionserver/has_joined.rs @@ -0,0 +1,19 @@ +/* + * 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 new file mode 100644 index 0000000..ceffb1e --- /dev/null +++ b/src/server/sessionserver/join.rs @@ -0,0 +1,19 @@ +/* + * 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/services/mod.rs b/src/server/sessionserver/mod.rs similarity index 84% rename from src/server/services/mod.rs rename to src/server/sessionserver/mod.rs index fe75014..db45c96 100644 --- a/src/server/services/mod.rs +++ b/src/server/sessionserver/mod.rs @@ -14,10 +14,17 @@ pub use tide::{Middleware, prelude::*, Request, Response, Result, utils::After}; pub use yggdrasil::*; +mod has_joined; +mod join; +mod profile; + 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("profile/:uuid").get(profile::profile); nest } diff --git a/src/server/sessionserver/profile.rs b/src/server/sessionserver/profile.rs new file mode 100644 index 0000000..3c8fdff --- /dev/null +++ b/src/server/sessionserver/profile.rs @@ -0,0 +1,19 @@ +/* + * 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 9814679..31e64f9 100644 --- a/src/util/database.rs +++ b/src/util/database.rs @@ -10,9 +10,12 @@ */ +use std::str::FromStr; +use std::time::Duration; + use anyhow::Result; -use sqlx::sqlite::SqlitePoolOptions; -use sqlx::SqlitePool; +use sqlx::{ConnectOptions, SqlitePool}; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; use crate::*; @@ -24,10 +27,14 @@ pub struct Database { impl Database { pub async fn init(config: Config) -> Result { + let mut options = SqliteConnectOptions::from_str(std::env::var("DATABASE_URL")?.as_str())?; + options.log_statements(log::LevelFilter::Off); + options.log_slow_statements(log::LevelFilter::Info, Duration::from_secs(2)); + Ok(Self { pool: SqlitePoolOptions::new() .max_connections(5) - .connect(std::env::var("DATABASE_URL")?.as_str()) + .connect_with(options) .await?, config, }) diff --git a/src/util/errors.rs b/src/util/errors.rs index 3ddb5db..3bb6ea7 100644 --- a/src/util/errors.rs +++ b/src/util/errors.rs @@ -13,6 +13,8 @@ use std::{error::Error, fmt}; use serde_json::json; +use crate::errors::YggdrasilErrorType::{BadRequestException, BaseYggdrasilException, ForbiddenOperationException, IllegalArgumentException}; + #[derive(Debug)] pub struct YggdrasilError(pub YggdrasilErrorType, pub String, pub u16, pub bool); // error type, cause, status code, do pass through @@ -48,4 +50,41 @@ impl YggdrasilError { "developerMessage": self.1.to_owned() }) } + + pub fn new_base(msg: &str) -> Self { + Self { + 0: BaseYggdrasilException, + 1: msg.to_string(), + 2: 500, + 3: false, + } + } + + pub fn new_forbidden(msg: &str) -> Self { + Self { + 0: ForbiddenOperationException, + 1: msg.to_string(), + 2: 403, + 3: false, + } + } + + pub fn new_bad_request(msg: &str) -> Self { + Self { + 0: BadRequestException, + 1: msg.to_string(), + 2: 400, + 3: false, + } + } + + pub fn new_illegal_argument(msg: &str) -> Self { + Self { + 0: IllegalArgumentException, + 1: msg.to_string(), + 2: 500, + 3: false + } + } + } diff --git a/src/util/structs.rs b/src/util/structs.rs deleted file mode 100644 index 7b3c791..0000000 --- a/src/util/structs.rs +++ /dev/null @@ -1,333 +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 std::time::UNIX_EPOCH; - -use json::{JsonValue, object}; -use serde::Deserialize; -use tide::convert::Serialize; - -use crate::*; - -// Account - -#[derive(Deserialize, Serialize, Debug)] -pub struct Account { - pub id: i64, - pub email: String, - pub password_hash: String, - pub language: String, - pub country: String, - pub selected_profile: Option, -} - -impl Account { - pub async fn from_id(db: &Database, id: i64) -> Option { - let record = sqlx::query!("SELECT * FROM accounts WHERE id = $1", id) - .fetch_one(&db.pool) - .await; - match record { - Ok(a) => Some(Self { - id, - email: a.email, - password_hash: a.password_hash, - language: a.language, - country: a.country, - selected_profile: match a.selected_profile { - None => None, - Some(profile_id) => Profile::from_id(db, profile_id).await, - }, - }), - Err(_) => None, - } - } -} - -// Attributes -// (technically not database struct but whatever) - -#[derive(Deserialize, Serialize, Debug)] -pub struct ProfileAttributes { - pub can_chat: bool, - pub can_play_multiplayer: bool, - pub can_play_realms: bool, - pub use_filter: bool, -} - -impl ProfileAttributes { - pub fn to_json(&self) -> JsonValue { - json::object! { - privileges: { - onlineChat: { enabled: self.can_chat }, - multiplayerServer: { enabled: self.can_play_multiplayer }, - multiplayerRealms: { enabled: self.can_play_realms }, - telemetry: { enabled: false }, - }, - profanityFilterPreferences: { - profanityFilterOn: self.use_filter - } - } - } -} - -// Blocked Server - -#[derive(Deserialize, Serialize, Debug)] -pub struct BlockedServer { - pub id: i64, - pub pattern: String, - pub sha1: String, - pub reason: Option, -} - -impl BlockedServer { - pub async fn from_id(db: &Database, id: i64) -> Option { - let record = sqlx::query!("SELECT * FROM blocked_servers WHERE id = $1", id) - .fetch_one(&db.pool) - .await; - match record { - Ok(s) => Some(Self { - id, - pattern: s.pattern, - sha1: s.sha1, - reason: s.reason, - }), - Err(_) => None, - } - } -} - -// Cape - -#[derive(Deserialize, Serialize, Debug)] -pub struct Cape { - pub id: i64, - pub friendly_id: String, - pub alias: String, -} - -impl Cape { - pub async fn from_id(db: &Database, id: i64) -> Option { - let record = sqlx::query!("SELECT * FROM capes WHERE id = $1", id) - .fetch_one(&db.pool) - .await; - match record { - Ok(c) => Some(Self { - id, - friendly_id: c.friendly_id, - alias: c.alias, - }), - Err(_) => None, - } - } -} - -// Profile - -#[derive(Deserialize, Serialize, Debug)] -pub struct Profile { - pub id: i64, - pub uuid: String, - - pub created: i64, // unix timestamp / 1000 - - pub owner: i64, - pub name: String, - pub name_history: String, - - pub skin_variant: String, - pub capes: Option>, - pub active_cape: Option, - - pub attributes: ProfileAttributes, -} - -impl Profile { - pub async fn from_id(db: &Database, id: i64) -> Option { - let record = sqlx::query!("SELECT * FROM profiles WHERE id = $1", id) - .fetch_one(&db.pool) - .await; - match record { - Ok(p) => Some(Self { - id, - uuid: p.uuid, - created: p.created, - owner: p.owner, - name: p.name, - name_history: p.name_history, - skin_variant: p.skin_variant, - capes: match p.capes { - None => None, - Some(capes) => Some( - json::parse(capes.as_str()) - .map(|c| { - serde_json::from_str::(c.to_string().as_str()) - .expect("Couldn't parse cape") - }) - .into_iter() - .collect(), - ), - }, - active_cape: match p.active_cape { - None => None, - Some(active_cape) => Cape::from_id(db, active_cape).await, - }, - attributes: serde_json::from_str(p.attributes.as_str()) - .expect("Couldn't parse profile attributes"), - }), - Err(_) => None, - } - } - - pub async fn get_skin(&self, db: &Database) -> Option { - // TODO: skin overrides - if self.skin_variant == "NONE" { - return None; - } - - Some(format!( - "{}/textures/skins/{}", - db.config.external_base_url, self.uuid - )) - } - - pub async fn get_cape(&self, db: &Database) -> Option { - // TODO: cape overrides - if self.active_cape.is_none() { - return None; - } - - let cape = self.active_cape.as_ref().unwrap(); - Some(format!( - "{}/textures/capes/{}", - db.config.external_base_url, cape.alias - )) - } -} - -// Session - -#[derive(Deserialize, Serialize, Debug)] -pub struct Session { - pub id: i64, - pub profile: Profile, - pub server_id: String, - pub ip_addr: String, -} - -impl Session { - pub async fn from_id(db: &Database, id: i64) -> Option { - let record = sqlx::query!("SELECT * FROM sessions WHERE id = $1", id) - .fetch_one(&db.pool) - .await; - match record { - Ok(s) => Some(Self { - id, - profile: Profile::from_id(db, s.profile).await.unwrap(), - server_id: s.server_id, - ip_addr: s.ip_addr, - }), - Err(_) => None, - } - } -} - -// Textures - -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!{} - }; - - 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 }; - } - } - - if profile.active_cape.is_some() { - let cape_url = profile.get_cape(db).await; - - if cape_url.is_some() { - object["textures"]["CAPE"] = object! { url: cape_url }; - } - } - - object! { - 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 { - 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 - } - } - - pub fn sign_textures(textures: &JsonValue) -> JsonValue { - // TODO: signing textures - unimplemented!() - } -} - -// Tokens - -pub struct Token { - id: i64, - access: String, - client: String, - account: Account, - issued: i64, - expires: i64, -} - -impl Token { - pub async fn from_id(db: &Database, id: i64) -> Option { - let record = sqlx::query!("SELECT * FROM tokens WHERE id = $1", id) - .fetch_one(&db.pool) - .await; - match record { - Ok(t) => Some(Self { - id, - access: t.access, - client: t.client, - account: Account::from_id(db, t.account) - .await - .expect("No account associated with token"), - issued: t.issued, - expires: t.expires, - }), - Err(_) => None, - } - } -} diff --git a/src/util/structs/Account.rs b/src/util/structs/Account.rs new file mode 100644 index 0000000..4a02952 --- /dev/null +++ b/src/util/structs/Account.rs @@ -0,0 +1,109 @@ +/* + * 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 serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + +use structs::Profile::{Profile, ProfileRaw}; + +use crate::*; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Account { + pub id: i64, + pub email: String, + pub password_hash: String, + pub language: String, + pub country: String, + pub selected_profile: Option, +} + +impl Account { + pub async fn from_id(db: &Database, id: i64) -> Option { + let record = sqlx::query_as!(AccountRaw, "SELECT * FROM accounts WHERE id = $1", id) + .fetch_one(&db.pool) + .await; + + match record { + Ok(r) => Some(r.complete(db).await), + Err(_) => None + } + } + + pub async fn from_email(db: &Database, email: String) -> Option { + let record = sqlx::query_as!(AccountRaw, "SELECT * FROM accounts WHERE email = $1", email) + .fetch_one(&db.pool) + .await; + + match record { + Ok(r) => Some(r.complete(db).await), + Err(_) => None, + } + } + + pub async fn get_all_profiles(&self, db: &Database) -> Option> { + let record = sqlx::query_as!(ProfileRaw, "SELECT * FROM profiles WHERE owner = $1", self.id).fetch_all(&db.pool).await; + + match record { + Ok(r) => { + let mut collection = vec![]; + for re in r { + collection.push(re.complete(db).await) + } + Some(collection) + } // oh boy + Err(_) => None + } + } + + pub fn to_user(&self) -> Value { + json!({ + "id": self.id, + "username": self.email, + "properties": [ + { + "name": "preferredLanguage", + "value": self.language + }, + { + "name": "registrationCountry", + "value": self.country + } + ] + }) + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct AccountRaw { + pub id: i64, + pub email: String, + pub password_hash: String, + pub language: String, + pub country: String, + pub selected_profile: Option +} + +impl AccountRaw { + pub async fn complete(self, db: &Database) -> Account { + Account { + id: self.id, + email: self.email, + password_hash: self.password_hash, + language: self.language, + country: self.country, + selected_profile: match self.selected_profile { + None => None, + Some(id) => Profile::from_id(db, id).await + }, + } + } +} \ No newline at end of file diff --git a/src/util/structs/BlockedServer.rs b/src/util/structs/BlockedServer.rs new file mode 100644 index 0000000..c9854d1 --- /dev/null +++ b/src/util/structs/BlockedServer.rs @@ -0,0 +1,35 @@ +/* + * 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 serde::{Deserialize, Serialize}; + +use crate::*; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct BlockedServer { + pub id: i64, + pub pattern: String, + pub sha1: String, + pub reason: Option, +} + +impl BlockedServer { + pub async fn from_id(db: &Database, id: i64) -> Option { + let record = sqlx::query_as!(BlockedServer, "SELECT * FROM blocked_servers WHERE id = $1", id) + .fetch_one(&db.pool) + .await; + + match record { + Ok(r) => Some(r), + Err(_) => None, + } + } +} \ No newline at end of file diff --git a/src/util/structs/Cape.rs b/src/util/structs/Cape.rs new file mode 100644 index 0000000..53bce8e --- /dev/null +++ b/src/util/structs/Cape.rs @@ -0,0 +1,38 @@ +/* + * 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 serde::{Deserialize, Serialize}; + +use crate::*; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Cape { + pub id: i64, + pub friendly_id: String, + pub alias: String, +} + +impl Cape { + pub async fn from_id(db: &Database, id: i64) -> Option { + let record = sqlx::query_as!(Cape, "SELECT * FROM capes WHERE id = $1", id) + .fetch_one(&db.pool) + .await; + + match record { + Ok(r) => Some(r), + Err(_) => None, + } + } + + pub fn capes_to_string(capes: Vec) -> String { + capes.into_iter().map(|c| c.alias).collect::>().join(",") + } +} \ No newline at end of file diff --git a/src/util/structs/Profile.rs b/src/util/structs/Profile.rs new file mode 100644 index 0000000..424a607 --- /dev/null +++ b/src/util/structs/Profile.rs @@ -0,0 +1,144 @@ +/* + * 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 serde::{Deserialize, Serialize}; + +use structs::{Cape::Cape, ProfileAttributes::ProfileAttributes}; + +use crate::*; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Profile { + pub id: i64, + pub uuid: String, + + pub created: i64, // unix timestamp / 1000 + + pub owner: i64, + pub name: String, + pub name_history: String, + + pub skin_variant: String, + pub capes: Option>, + pub active_cape: Option, + + pub attributes: ProfileAttributes, +} + +impl Profile { + pub async fn from_id(db: &Database, id: i64) -> Option { + let record = sqlx::query_as!(ProfileRaw, "SELECT * FROM profiles WHERE id = $1", id) + .fetch_one(&db.pool) + .await; + + match record { + Ok(r) => Some(r.complete(db).await), + Err(_) => None, + } + } + + pub async fn from_uuid(db: &Database, uuid: String) -> Option { + let record = sqlx::query_as!(ProfileRaw, "SELECT * FROM profiles WHERE uuid = $1", uuid) + .fetch_one(&db.pool) + .await; + + match record { + Ok(r) => Some(r.complete(db).await), + Err(_) => None + } + } + + pub async fn from_name(db: &Database, name: String) -> Option { + let record = sqlx::query_as!(ProfileRaw, "SELECT * FROM profiles WHERE name = $1", name) + .fetch_one(&db.pool) + .await; + + match record { + Ok(r) => Some(r.complete(db).await), + Err(_) => None + } + } + + pub async fn get_skin(&self, db: &Database) -> Option { + // TODO: skin overrides + if self.skin_variant == "NONE" { + return None; + } + + Some(format!( + "{}/textures/skins/{}", + db.config.external_base_url, self.uuid + )) + } + + pub async fn get_cape(&self, db: &Database) -> Option { + // TODO: cape overrides + if self.active_cape.is_none() { + return None; + } + + let cape = self.active_cape.as_ref().unwrap(); + Some(format!( + "{}/textures/capes/{}", + db.config.external_base_url, cape.alias + )) + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct ProfileRaw { + pub id: i64, + pub uuid: String, + + pub created: i64, + + pub owner: i64, + pub name: String, + pub name_history: String, + + pub skin_variant: String, + pub capes: Option, + pub active_cape: Option, + + pub attributes: String, +} + +impl ProfileRaw { + pub async fn complete(self, db: &Database) -> Profile { + Profile { + id: self.id, + uuid: self.uuid, + created: self.created, + owner: self.owner, + name: self.name, + name_history: self.name_history, + skin_variant: self.skin_variant, + capes: match self.capes { + None => None, + Some(capes) => Some( + json::parse(capes.as_str()) + .map(|c| { + serde_json::from_str::(c.to_string().as_str()) + .expect("Couldn't parse cape") + }) + .into_iter() + .collect(), + ), + }, + active_cape: match self.active_cape { + 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"), + } + } +} \ No newline at end of file diff --git a/src/util/structs/ProfileAttributes.rs b/src/util/structs/ProfileAttributes.rs new file mode 100644 index 0000000..755ef9a --- /dev/null +++ b/src/util/structs/ProfileAttributes.rs @@ -0,0 +1,38 @@ +/* + * 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 serde_json::{json, Value}; +use serde::{Deserialize, Serialize}; + + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct ProfileAttributes { + pub can_chat: bool, + pub can_play_multiplayer: bool, + pub can_play_realms: bool, + pub use_filter: bool, +} + +impl ProfileAttributes { + pub fn to_json(&self) -> Value { + json!({ + "privileges": { + "onlineChat": { "enabled": self.can_chat }, + "multiplayerServer": { "enabled": self.can_play_multiplayer }, + "multiplayerRealms": { "enabled": self.can_play_realms }, + "telemetry": { "enabled": false }, + }, + "profanityFilterPreferences": { + "profanityFilterOn": self.use_filter + } + }) + } +} diff --git a/src/util/structs/Session.rs b/src/util/structs/Session.rs new file mode 100644 index 0000000..fa98b5e --- /dev/null +++ b/src/util/structs/Session.rs @@ -0,0 +1,57 @@ +/* + * 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 serde::{Deserialize, Serialize}; + +use structs::Profile::Profile; + +use crate::*; + +#[derive(Deserialize, Serialize, Debug)] +pub struct Session { + pub id: i64, + pub profile: Profile, + pub server_id: String, + pub ip_addr: String, +} + +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) + .fetch_one(&db.pool) + .await; + + match record { + Ok(r) => Some(r.complete(db).await), + Err(_) => None, + } + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct RawSession { + pub id: i64, + pub profile: i64, + pub server_id: String, + pub ip_addr: String +} + +impl RawSession { + pub async fn complete(self, db: &Database) -> Session { + Session { + id: self.id, + profile: Profile::from_id(db, self.profile).await.expect("Couldn't resolve session profile"), + server_id: self.server_id, + ip_addr: self.ip_addr, + } + } +} + diff --git a/src/util/structs/TexturedObject.rs b/src/util/structs/TexturedObject.rs new file mode 100644 index 0000000..9a1bf94 --- /dev/null +++ b/src/util/structs/TexturedObject.rs @@ -0,0 +1,77 @@ +/* + * 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::time::UNIX_EPOCH; + +use serde::{Deserialize, Serialize}; +use json::{object, JsonValue}; +use structs::Profile::Profile; + +use crate::*; + +#[derive(Deserialize, Serialize, Debug)] +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!{} + }; + + 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 }; + } + } + + if profile.active_cape.is_some() { + let cape_url = profile.get_cape(db).await; + + if cape_url.is_some() { + object["textures"]["CAPE"] = object! { url: cape_url }; + } + } + + object! { + 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 { + 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 + } + } + + pub fn sign_textures(textures: &JsonValue) -> JsonValue { + // TODO: signing textures + unimplemented!() + } +} \ No newline at end of file diff --git a/src/util/structs/Token.rs b/src/util/structs/Token.rs new file mode 100644 index 0000000..b2a5b04 --- /dev/null +++ b/src/util/structs/Token.rs @@ -0,0 +1,64 @@ +/* + * 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 serde::{Deserialize, Serialize}; + +use structs::Account::Account; + +use crate::*; + +#[derive(Deserialize, Serialize, Debug)] +pub struct Token { + id: i64, + access: String, + client: String, + account: Account, + issued: i64, + expires: i64, +} + +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) + .fetch_one(&db.pool) + .await; + match record { + Ok(t) => Some(t.complete(db).await), + Err(_) => None, + } + } + + pub fn random_token() -> String { + random_string::generate(128, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_.") + } +} + +pub struct RawToken { + id: i64, + access: String, + client: String, + account: i64, + issued: i64, + expires: i64 +} + +impl RawToken { + pub async fn complete(self, db: &Database) -> Token { + Token { + id: self.id, + access: self.access, + client: self.client, + account: Account::from_id(db, self.account).await.expect("Couldn't resolve token owner"), + issued: self.issued, + expires: self.expires, + } + } +} diff --git a/src/util/structs/mod.rs b/src/util/structs/mod.rs new file mode 100644 index 0000000..71ef60c --- /dev/null +++ b/src/util/structs/mod.rs @@ -0,0 +1,21 @@ +/* + * 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 . + */ + +// yeah yeah snake_case whatever +pub mod Account; +pub mod BlockedServer; +pub mod Cape; +pub mod Profile; +pub mod ProfileAttributes; +pub mod Session; +pub mod TexturedObject; +pub mod Token; +