feat: add user-agent arg, moved actions in file

This commit is contained in:
əlemi 2023-11-20 02:08:24 +01:00
parent 1808e98450
commit 0f310896d7
Signed by: alemi
GPG key ID: A4895B84D311642C
4 changed files with 91 additions and 58 deletions

1
src/admin/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod user;

63
src/admin/user.rs Normal file
View file

@ -0,0 +1,63 @@
use std::collections::HashSet;
use reqwest::header::USER_AGENT;
use sea_orm::{DatabaseConnection, DbErr, EntityTrait, ActiveValue::NotSet, Set};
use crate::{entities::{user, property}, proto};
pub async fn user_fetch_skin(db: &DatabaseConnection, user: &user::Model, agent: &str) -> Result<(), DbErr> {
tracing::info!("Updating skin for user {} ({})", user.name, user.uuid);
let url = format!("https://sessionserver.mojang.com/session/minecraft/profile/{}?unsigned=false", user.uuid.simple());
let res = reqwest::Client::new()
.get(url.clone()) //TODO: needs trimmed uuid, is it trimmed by default?
.header(USER_AGENT, agent)
.send().await
.map_err(|_| DbErr::Custom(format!("could not fetch profile for user {}", user.uuid)))?
.text().await
.expect("invalid body on response");
let doc = serde_json::from_str::<proto::SessionUser>(&res)
.map_err(|_| DbErr::Json(format!("invalid texture response: {}", res)))?;
let mut skin = proto::Property::default_skin();
for s in doc.properties.expect("missing properties field") {
if s.name == "textures" {
skin = s;
break;
}
}
property::Entity::insert(
property::ActiveModel {
id: NotSet,
name: Set("textures".into()),
user_id: Set(user.id),
value: Set(skin.value),
signature: Set(skin.signature),
}
).exec(db).await?;
Ok(())
}
pub async fn user_fetch_skin_all(db: &DatabaseConnection, agent: &str) -> Result<(), DbErr> {
let mut users_with_skins = HashSet::new();
for skin in property::Entity::find().all(db).await? {
users_with_skins.insert(skin.user_id);
}
for user in user::Entity::find().all(db).await? {
if users_with_skins.contains(&user.id) {
continue;
}
if let Err(e) = user_fetch_skin(db, &user, agent).await {
tracing::error!("could not fetch skin for '{}': {}", user.name, e);
}
}
Ok(())
}

View file

@ -1,6 +1,7 @@
mod proto; mod proto;
mod entities; mod entities;
mod routes; mod routes;
mod admin;
mod persistence; mod persistence;
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
@ -8,7 +9,7 @@ use std::{collections::HashMap, sync::Arc};
use chrono::{DateTime, Utc, Duration}; use chrono::{DateTime, Utc, Duration};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use axum::{Router, routing::{get, post}, response::IntoResponse, Json, http::StatusCode}; use axum::{Router, routing::{get, post}, response::IntoResponse, Json, http::StatusCode};
use routes::register::fill_missing_skins; use admin::user::user_fetch_skin_all;
use sea_orm::{DatabaseConnection, Database}; use sea_orm::{DatabaseConnection, Database};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tracing_subscriber::filter::filter_fn; use tracing_subscriber::filter::filter_fn;
@ -31,6 +32,10 @@ struct CliArgs {
/// Action to run /// Action to run
#[clap(subcommand)] #[clap(subcommand)]
action: CliAction, action: CliAction,
/// User agent to send when fetching minecraft data
#[arg(short, long)]
user_agent: Option<String>,
} }
#[derive(Subcommand, Clone, Debug)] #[derive(Subcommand, Clone, Debug)]
@ -88,6 +93,7 @@ pub struct AppState {
token_duration: u32, token_duration: u32,
time_window: u32, time_window: u32,
fallback: bool, fallback: bool,
user_agent: String,
} }
#[tokio::main] #[tokio::main]
@ -102,12 +108,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let db = Database::connect(cfg.database.clone()).await?; let db = Database::connect(cfg.database.clone()).await?;
let user_agent = cfg.user_agent.unwrap_or(
format!("yggdrasil/{}", env!("CARGO_PKG_VERSION"))
);
match cfg.action { match cfg.action {
CliAction::Clean { token_lifetime } => { CliAction::Clean { token_lifetime } => {
purge_expired_tokens(&db, Duration::hours(token_lifetime.into())).await?; purge_expired_tokens(&db, Duration::hours(token_lifetime.into())).await?;
}, },
CliAction::Skins { } => { CliAction::Skins { } => {
fill_missing_skins(&db).await?; user_fetch_skin_all(&db, &user_agent).await?;
}, },
CliAction::Serve { bind_addr, token_duration, time_window, fallback } => { CliAction::Serve { bind_addr, token_duration, time_window, fallback } => {
let secret = load_secret(&db).await?; let secret = load_secret(&db).await?;
@ -128,7 +138,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// CUSTOM // CUSTOM
.route("/register/unmigrated", post(register_unmigrated)) .route("/register/unmigrated", post(register_unmigrated))
.fallback(fallback_route) .fallback(fallback_route)
.with_state(AppState { store, db, token_duration, time_window, fallback, secret }); .with_state(AppState { store, db, token_duration, time_window, fallback, secret, user_agent });
info!(target: "MAIN", "serving Yggdrasil on {}", &addr); info!(target: "MAIN", "serving Yggdrasil on {}", &addr);

View file

@ -1,58 +1,12 @@
use std::{time::Duration, collections::HashSet}; use std::time::Duration;
use axum::{extract::State, Json}; use axum::{extract::State, Json};
use reqwest::StatusCode; use reqwest::{StatusCode, header::USER_AGENT};
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, Set, ActiveValue::NotSet, DatabaseConnection, DbErr}; use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, Set, ActiveValue::NotSet};
use tracing::info; use tracing::info;
use crate::{AppState, proto::{self, Response}, entities}; use crate::{AppState, proto::{self, Response}, entities};
// TODO shouldn't be here probably
pub async fn fill_missing_skins(db: &DatabaseConnection) -> Result<(), DbErr> {
let mut users_with_skins = HashSet::new();
for skin in entities::property::Entity::find().all(db).await? {
users_with_skins.insert(skin.user_id);
}
for user in entities::user::Entity::find().all(db).await? {
if users_with_skins.contains(&user.id) {
continue;
}
info!("Updating skin for user {} ({})", user.name, user.uuid);
let url = format!("https://sessionserver.mojang.com/session/minecraft/profile/{}?unsigned=false", user.uuid.simple());
let res = reqwest::Client::new().get(url.clone()) //TODO: needs trimmed uuid, is it trimmed by default?
.json(&()).send()
.await.map_err(|_| DbErr::Custom(format!("could not fetch profile for user {}", user.uuid)))?
.text().await.expect("invalid body on response");
let doc = serde_json::from_str::<proto::SessionUser>(&res)
.map_err(|_| DbErr::Json("invalid texture response".into()))?;
let mut skin = proto::Property::default_skin();
for s in doc.properties.expect("missing properties field") {
if s.name == "textures" {
skin = s;
break;
}
}
entities::property::Entity::insert(
entities::property::ActiveModel {
id: NotSet,
name: Set("textures".into()),
user_id: Set(user.id),
value: Set(skin.value),
signature: Set(skin.signature),
}
).exec(db).await?;
}
Ok(())
}
//TODO: replace format! with axum's own //TODO: replace format! with axum's own
pub async fn register_unmigrated(State(state): State<AppState>, Json(payload): Json<proto::RegisterRequest>) -> Response<proto::RegisterResponse> { pub async fn register_unmigrated(State(state): State<AppState>, Json(payload): Json<proto::RegisterRequest>) -> Response<proto::RegisterResponse> {
info!(target: "REGISTER", "[UNMIGRATED] called with {:?}", payload); info!(target: "REGISTER", "[UNMIGRATED] called with {:?}", payload);
@ -60,11 +14,13 @@ pub async fn register_unmigrated(State(state): State<AppState>, Json(payload): J
let form = proto::RefreshRequest { let form = proto::RefreshRequest {
accessToken: payload.token.accessToken, accessToken: payload.token.accessToken,
clientToken: payload.token.clientToken, clientToken: payload.token.clientToken,
selectedProfile: Some(payload.token.selectedProfile.clone()), selectedProfile: None,
requestUser: Some(true), requestUser: Some(true),
}; };
let response = reqwest::Client::new().post("https://authserver.mojang.com/refresh") let response = reqwest::Client::new()
.post("https://authserver.mojang.com/refresh")
.header(USER_AGENT, &state.user_agent)
.json(&form) .json(&form)
.send().await .send().await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(proto::Error::simple(format!("mojang error : {:?}", e)))))? .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(proto::Error::simple(format!("mojang error : {:?}", e)))))?
@ -99,10 +55,13 @@ pub async fn register_unmigrated(State(state): State<AppState>, Json(payload): J
tokio::time::sleep(Duration::from_millis(500)).await; // avoid errors fetching skin due to timings tokio::time::sleep(Duration::from_millis(500)).await; // avoid errors fetching skin due to timings
let url = format!("https://sessionserver.mojang.com/session/minecraft/profile/{}?unsigned=false", uuid.simple()); let url = format!("https://sessionserver.mojang.com/session/minecraft/profile/{}?unsigned=false", uuid.simple());
let res = reqwest::Client::new().get(url.clone()) //TODO: needs trimmed uuid, is it trimmed by default? let res = reqwest::Client::new()
.json(&()).send() .get(url.clone()) //TODO: needs trimmed uuid, is it trimmed by default?
.await.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json(proto::Error::simple("internal server error"))))? .header(USER_AGENT, &state.user_agent)
.text().await.expect("invalid body on response"); .send().await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, Json(proto::Error::simple("internal server error"))))?
.text().await
.expect("invalid body on response");
info!(target:"REGISTER", "Mojang response to texture fetch [{}]: {}", url, &res); info!(target:"REGISTER", "Mojang response to texture fetch [{}]: {}", url, &res);
let doc = serde_json::from_str::<proto::SessionUser>(&res) let doc = serde_json::from_str::<proto::SessionUser>(&res)