334 lines
9.2 KiB
Rust
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,
|
|
}
|
|
}
|
|
}
|