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"