yggdrasil/src/util/structs.rs

334 lines
9.2 KiB
Rust

/*
* Yggdrasil: Minecraft authentication server
* Copyright (C) 2023 0xf8.dev@proton.me
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::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<Profile>,
}
impl Account {
pub async fn from_id(db: &Database, id: i64) -> Option<Self> {
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<String>,
}
impl BlockedServer {
pub async fn from_id(db: &Database, id: i64) -> Option<Self> {
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<Self> {
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<Vec<Cape>>,
pub active_cape: Option<Cape>,
pub attributes: ProfileAttributes,
}
impl Profile {
pub async fn from_id(db: &Database, id: i64) -> Option<Self> {
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::<Cape>(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<String> {
// 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<String> {
// 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<Self> {
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<Self> {
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,
}
}
}