diff --git a/.gitignore b/.gitignore index f7f2d24..c7b252e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,7 @@ Cargo.lock yggdrasilrc yggdrasilrc.old yggdrasil.db* +priv.key +publ.key .env \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 9c093f0..2357303 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,10 +9,17 @@ license = "GPL3.0-or-later" [dependencies] anyhow = "1.0.71" +base64 = "0.21.2" colored = "2.0.0" driftwood = "0.0.7" -ftlog = "0.2.7" +femme = "2.2.1" +json = "0.12.4" +log = "0.4.19" +rand = "0.8.5" +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"] } tide = "0.16.0" time = "0.3.22" diff --git a/migrations/0_yggdrasil.sql b/migrations/0_yggdrasil.sql new file mode 100644 index 0000000..07881bb --- /dev/null +++ b/migrations/0_yggdrasil.sql @@ -0,0 +1,55 @@ +CREATE TABLE IF NOT EXISTS accounts ( + id INTEGER PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + language TEXT NOT NULL, + country TEXT NOT NULL, + selected_profile INTEGER, + FOREIGN KEY(selected_profile) REFERENCES profiles(id) +); + +CREATE TABLE IF NOT EXISTS blocked_servers ( + id INTEGER PRIMARY KEY, + pattern TEXT NOT NULL UNIQUE, + sha1 TEXT NOT NULL UNIQUE, + reason TEXT +); + +CREATE TABLE IF NOT EXISTS capes ( + id INTEGER PRIMARY KEY, + friendly_id TEXT NOT NULL UNIQUE, + alias TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS profiles ( + id INTEGER PRIMARY KEY, + uuid TEXT NOT NULL UNIQUE, + created INTEGER NOT NULL, + owner INTEGER NOT NULL, + name TEXT NOT NULL UNIQUE, + name_history TEXT NOT NULL, + skin_variant TEXT NOT NULL, + capes TEXT, + active_cape INTEGER, + attributes TEXT NOT NULL, + FOREIGN KEY(owner) REFERENCES accounts(id), + FOREIGN KEY(active_cape) REFERENCES capes(id) +); + +CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY, + profile INTEGER NOT NULL, + server_id TEXT NOT NULL, + ip_addr TEXT NOT NULL, + FOREIGN KEY(profile) REFERENCES profiles(id) +); + +CREATE TABLE IF NOT EXISTS tokens ( + id INTEGER PRIMARY KEY, + access TEXT NOT NULL UNIQUE, + client TEXT NOT NULL, + account INTEGER NOT NULL, + issued INTEGER NOT NULL, + expires INTEGER NOT NULL, + FOREIGN KEY(account) REFERENCES accounts(id) +); \ No newline at end of file diff --git a/src/database.rs b/src/database.rs deleted file mode 100644 index 9355eaf..0000000 --- a/src/database.rs +++ /dev/null @@ -1,62 +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::env; -use std::path::PathBuf; - -use anyhow::Result; -use ftlog::{debug, error, info, log, trace, warn}; -use sqlx::sqlite::SqlitePoolOptions; -use sqlx::SqlitePool; - -use crate::config::Config; - -pub struct Database { - db: SqlitePool, -} - -impl Database { - pub async fn init(config: &Config) -> Result { - let pool = SqlitePoolOptions::new().max_connections(5).connect("").await?; - - Ok(Self { - db: pool - }) - } - - async fn open(config: &Config) -> Result { - Ok(SqlitePoolOptions::new().max_connections(5).connect(&env::var("DATABASE_URL")?).await?) - } - - async fn new(config: &Config) -> Result { - let pool = Self::open(&config).await?; - pool. - let mut conn = pool.acquire().await?; - - sqlx::query!(r#" - - "#).execute(&mut conn).await?; - - pool.prepare("").await?; - - unimplemented!() - } - - async fn ensure_db(config: &Config) -> Result { - let db_path = PathBuf::from(&env::var("DATABASE_URL")?); - - match db_path.try_exists()? { - true => Self::open(&config).await, - false => Self::new(&config).await - } - } -} - diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b5fe741 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,17 @@ +/* + * 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)] + +pub use util::*; + +mod util; + diff --git a/src/main.rs b/src/main.rs index d5977d4..5b3e4ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,40 +11,39 @@ #![feature(fs_try_exists)] -use anyhow::Result; -use ftlog::{debug, error, info, log, trace, warn}; +use anyhow::{bail, Result}; +use log::{debug, error, info, log, trace, warn}; use tokio::spawn; -use crate::config::Config; -use crate::server::start_server; +use yggdrasil::*; -mod database; mod server; -mod config; + #[tokio::main] async fn main() -> Result<()> { - // Build logger - let time_format = time::format_description::parse_owned::<1>("[[[year]-[month]-[day] [hour]:[minute]:[second]]")?; + // Early catch + if std::env::var("DATABASE_URL").is_err() { + bail!("DATABASE_URL needs to be set.") + } - ftlog::builder() - .time_format(time_format) - .bounded(100_000, false) - .try_init() - .expect("Failed to initialize logger"); + // Start logger + femme::start(); // Load config let config = Config::load()?; info!("Config location: {}", config.location.display()); - // Start server - info!("Starting yggdrasil server!"); + // Load database + let db = Database::init(config).await?; + info!("Database URL: {}", std::env::var("DATABASE_URL")?); - let server_thread = spawn(start_server(config.to_owned())); + // Start server + let server_thread = spawn(server::start(db)); server_thread.await??; - - info!("Server stopped!"); + + warn!("Server stopped!"); // Cleanup - Ok(ftlog::logger().flush()) + Ok(log::logger().flush()) } diff --git a/src/server/account/mod.rs b/src/server/account/mod.rs new file mode 100644 index 0000000..fe75014 --- /dev/null +++ b/src/server/account/mod.rs @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +pub use log::{info, log, warn}; +pub use tide::{Middleware, prelude::*, Request, Response, Result, utils::After}; + +pub use yggdrasil::*; + +pub fn nest(db: Database) -> tide::Server { + info!("Loading nest"); + + let mut nest = tide::with_state(db); + + nest +} diff --git a/src/server/auth/mod.rs b/src/server/auth/mod.rs new file mode 100644 index 0000000..fe75014 --- /dev/null +++ b/src/server/auth/mod.rs @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +pub use log::{info, log, warn}; +pub use tide::{Middleware, prelude::*, Request, Response, Result, utils::After}; + +pub use yggdrasil::*; + +pub fn nest(db: Database) -> tide::Server { + info!("Loading nest"); + + let mut nest = tide::with_state(db); + + nest +} diff --git a/src/server/authlib/mod.rs b/src/server/authlib/mod.rs new file mode 100644 index 0000000..0a88a84 --- /dev/null +++ b/src/server/authlib/mod.rs @@ -0,0 +1,42 @@ +/* + * 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 . + */ + +pub use log::{info, log, warn}; +pub use tide::{Middleware, prelude::*, Request, Response, Result, utils::After}; + +pub use yggdrasil::*; + +pub fn nest(db: Database) -> tide::Server { + info!("Loading nest"); + + let mut nest = tide::with_state(db); + + nest.at("/").get(authlib_meta); + + nest +} + +async fn authlib_meta(req: Request) -> Result { + let config = &req.state().config; + Ok(json!({ + "meta": { + "implementationName": std::env!("CARGO_PKG_NAME"), + "implementationVersion": std::env!("CARGO_PKG_VERSION"), + "feature.no_mojang_namespace": config.no_mojang_namespace, + "links": { + "homepage": config.external_base_url + }, + "serverName": config.server_name, + }, + "skinDomains": config.skin_domains + // TODO: public key signature + }).into()) +} \ No newline at end of file diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..4b3b7bc --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,56 @@ +/* + * 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::{Request, Response, utils::After}; + +use yggdrasil::*; + +mod account; +mod auth; +mod authlib; +mod services; +mod session; + +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::() { + let body = err.to_json(); + res.set_status(err.2); + res.set_body(body); + + // TODO: pass through + // err.3: bool + } + + Ok(res) + })); + + // Index + app.at("/").get(|mut req: Request| async move { + req.append_header("x-authlib-injector-api-location", "/authlib/"); + Ok("Yggdrasil") + }); + + // 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())); + + // Listen + app.listen(format!("{}:{}", &db.config.address, &db.config.port)).await?; + + Ok(()) +} \ No newline at end of file diff --git a/src/server/services/mod.rs b/src/server/services/mod.rs new file mode 100644 index 0000000..fe75014 --- /dev/null +++ b/src/server/services/mod.rs @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +pub use log::{info, log, warn}; +pub use tide::{Middleware, prelude::*, Request, Response, Result, utils::After}; + +pub use yggdrasil::*; + +pub fn nest(db: Database) -> tide::Server { + info!("Loading nest"); + + let mut nest = tide::with_state(db); + + nest +} diff --git a/src/server/session/mod.rs b/src/server/session/mod.rs new file mode 100644 index 0000000..fe75014 --- /dev/null +++ b/src/server/session/mod.rs @@ -0,0 +1,23 @@ +/* + * 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 . + */ + +pub use log::{info, log, warn}; +pub use tide::{Middleware, prelude::*, Request, Response, Result, utils::After}; + +pub use yggdrasil::*; + +pub fn nest(db: Database) -> tide::Server { + info!("Loading nest"); + + let mut nest = tide::with_state(db); + + nest +} diff --git a/src/config.rs b/src/util/config.rs similarity index 69% rename from src/config.rs rename to src/util/config.rs index 9eccaac..3d1f31a 100644 --- a/src/config.rs +++ b/src/util/config.rs @@ -14,15 +14,35 @@ use std::path::PathBuf; use std::str::FromStr; use anyhow::Result; -use ftlog::{debug, error, info, log, trace, warn}; +use log::{debug, warn}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { #[serde(skip)] pub location: PathBuf, + + #[serde(rename = "http.port")] pub port: i16, + #[serde(rename = "http.address")] pub address: String, + #[serde(rename = "http.external_base_url")] + pub external_base_url: String, + + #[serde(rename = "authlib.server_name")] + pub server_name: String, + #[serde(rename = "authlib.no_mojang_namespace")] + pub no_mojang_namespace: bool, + #[serde(rename = "authlib.skin_domains")] + pub skin_domains: Vec, + + #[serde(rename = "signing.bits")] + pub key_bits: u64, + #[serde(rename = "signing.priv.location")] + pub key_priv_location: PathBuf, + #[serde(rename = "signing.publ.location")] + pub key_publ_location: PathBuf, + } impl Default for Config { @@ -31,8 +51,22 @@ impl Default for Config { location: std::env::current_dir() .expect("Couldn't get current directory") .join("yggdrasilrc"), + port: 8081, - address: "0.0.0.0".parse().expect("Couldn't parse string"), + address: "0.0.0.0".to_string(), + external_base_url: format!("http://localhost:8081"), + + server_name: "Yggdrasil".to_string(), + no_mojang_namespace: true, + skin_domains: vec!["localhost:8081".to_string(), ".localhost:8081".to_string()], + + key_bits: 4096, + key_priv_location: std::env::current_dir() + .expect("Couldn't get current directory") + .join("priv.key"), + key_publ_location: std::env::current_dir() + .expect("Couldn't get current directory") + .join("publ.key"), } } } diff --git a/src/server.rs b/src/util/database.rs similarity index 61% rename from src/server.rs rename to src/util/database.rs index 9636f8e..9814679 100644 --- a/src/server.rs +++ b/src/util/database.rs @@ -9,24 +9,27 @@ * You should have received a copy of the GNU General Public License along with this program. If not, see . */ + use anyhow::Result; -use ftlog::{debug, error, info, log, trace, warn}; -use tide::prelude::*; -use tide::Request; +use sqlx::sqlite::SqlitePoolOptions; +use sqlx::SqlitePool; -use crate::config::Config; +use crate::*; -pub async fn start_server(config: Config) -> Result<()> { - let mut app = tide::new(); - app.with(driftwood::DevLogger); - - app.at("/test").get(test); - - app.listen(format!("{}:{}", config.address, config.port)).await?; - - Ok(()) +#[derive(Clone, Debug)] +pub struct Database { + pub pool: SqlitePool, + pub config: Config, } -async fn test(mut req: Request<()>) -> tide::Result { - Ok(format!("Hello, world!").into()) +impl Database { + pub async fn init(config: Config) -> Result { + Ok(Self { + pool: SqlitePoolOptions::new() + .max_connections(5) + .connect(std::env::var("DATABASE_URL")?.as_str()) + .await?, + config, + }) + } } diff --git a/src/util/errors.rs b/src/util/errors.rs new file mode 100644 index 0000000..3ddb5db --- /dev/null +++ b/src/util/errors.rs @@ -0,0 +1,51 @@ +/* + * 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::{error::Error, fmt}; + +use serde_json::json; + +#[derive(Debug)] +pub struct YggdrasilError(pub YggdrasilErrorType, pub String, pub u16, pub bool); +// error type, cause, status code, do pass through + +#[derive(Debug)] +pub enum YggdrasilErrorType { + BaseYggdrasilException, + ForbiddenOperationException, + BadRequestException, + IllegalArgumentException, +} + +impl Error for YggdrasilError {} + +impl fmt::Display for YggdrasilError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use YggdrasilErrorType::*; + + match self.0 { + ForbiddenOperationException => write!(f, "FORBIDDEN"), + BadRequestException => write!(f, "BAD_REQUEST"), + _ => write!(f, "INTERNAL_SERVER_ERROR"), + } + } +} + +impl YggdrasilError { + pub fn to_json(&self) -> serde_json::Value { + json!({ + "errorType": format!("{}", self), + "error": format!("{:?}", self.0), + "errorMessage": self.1.to_owned(), + "developerMessage": self.1.to_owned() + }) + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..7abc52c --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,22 @@ +/* + * 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 . + */ + +pub use config::Config; +pub use database::Database; + +mod config; +// TODO: fix signing +// https://github.com/RustCrypto/RSA/blob/master/tests/proptests.rs +// mod signing; +mod database; +pub mod errors; +pub mod structs; + diff --git a/src/util/signing.rs b/src/util/signing.rs new file mode 100644 index 0000000..03b9de4 --- /dev/null +++ b/src/util/signing.rs @@ -0,0 +1,91 @@ +/* + * 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::fs::{read, read_to_string, try_exists, write}; + +use anyhow::Result; +use rsa::{ + Oaep, + pkcs1::DecodeRsaPublicKey, + pkcs1::LineEnding, + pkcs1v15, + pkcs1v15::SigningKey, + pkcs8::{EncodePrivateKey, EncodePublicKey}, + pkcs8::DecodePrivateKey, + RsaPrivateKey, + RsaPublicKey, + signature::{SignatureEncoding, Signer} +}; + +use crate::Config; + +#[derive(Debug, Clone)] +pub struct Signing { + private: RsaPrivateKey, + pub public: RsaPublicKey, + pub signing: SigningKey +} + +impl Signing { + pub fn init(config: &Config) -> Result { + Self::ensure(config) + } + + fn ensure(config: &Config) -> Result { + if config.key_publ_location.try_exists()? && config.key_priv_location.try_exists()? { + Self::load(config) + } else { + Self::new(config) + } + } + + fn new(config: &Config) -> Result { + let mut rng = rand::thread_rng(); + + let private = RsaPrivateKey::new(&mut rng, 4096).expect("Couldn't generate rsa key"); + let public = RsaPublicKey::from(&private); + let signing = SigningKey::::new(private.to_owned()); + + let signing = Self { private, public, signing }; + + signing.save(config)?; + + Ok(signing) + } + + fn load(config: &Config) -> Result { + let private = DecodePrivateKey::read_pkcs8_der_file(config.key_priv_location.to_owned())?; + let public = DecodeRsaPublicKey::read_pkcs1_pem_file(config.key_publ_location.to_owned())?; + let signing = SigningKey::::new(private.to_owned()); + + Ok(Self { private, public, signing }) + } + + pub fn save(&self, config: &Config) -> Result<()> { + let der = self.private.to_pkcs8_der()?; + der.write_der_file(config.key_priv_location.to_owned())?; + + let pem = self.public.to_public_key_pem(LineEnding::LF)?; + write(config.key_publ_location.to_owned(), pem)?; + + Ok(()) + } + + pub fn sign(&self, data: &[u8]) -> Result> { + // let pad = Pss::new(); + // Ok(self.private.sign(pad, data)?) + + // Ok(self.signing) + + todo!() + } + +} \ No newline at end of file diff --git a/src/util/structs.rs b/src/util/structs.rs new file mode 100644 index 0000000..7b3c791 --- /dev/null +++ b/src/util/structs.rs @@ -0,0 +1,333 @@ +/* + * 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/yggdrasilrc.old b/yggdrasilrc.old deleted file mode 100644 index 297109a..0000000 --- a/yggdrasilrc.old +++ /dev/null @@ -1,2 +0,0 @@ -port = 8081 -address = "0.0.0.0"