1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-05-03 11:37:45 +00:00
kawari/src/bin/kawari-patch.rs
Joshua Goins 1e343d0f10 Add version checks in the patch server for game and boot components
As 7.2 is releasing next week, it would be nice for the patch server to double
check the user has the correct version of the game. Now the patch server rejects
clients that have too new of a version.
2025-03-23 08:21:43 -04:00

215 lines
7.1 KiB
Rust

use std::cmp::Ordering;
use std::fs::read_dir;
use axum::extract::Path;
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use axum::routing::post;
use axum::{Router, routing::get};
use kawari::config::get_config;
use kawari::patch::Version;
use kawari::{SUPPORTED_BOOT_VERSION, SUPPORTED_GAME_VERSION, get_supported_expac_versions};
use physis::patchlist::{PatchEntry, PatchList, PatchListType};
fn list_patch_files(dir_path: &str) -> Vec<String> {
// If the dir doesn't exist, pretend there is no patch files
let Ok(dir) = read_dir(dir_path) else {
return Vec::new();
};
let mut entries: Vec<_> = dir.flatten().collect();
entries.sort_by_key(|dir| dir.path());
let mut game_patches: Vec<_> = entries
.into_iter()
.flat_map(|entry| {
let Ok(meta) = entry.metadata() else {
return vec![];
};
if meta.is_dir() {
return vec![];
}
if meta.is_file() && entry.file_name().to_str().unwrap().contains(".patch") {
return vec![entry.path()];
}
vec![]
})
.collect();
game_patches.sort_by(|a, b| {
// Ignore H/D in front of filenames
let a_path = a
.as_path()
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string();
if a_path.starts_with("H") {
return Ordering::Less;
}
let b_path = b
.as_path()
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string();
/*if b_path.starts_with("H") {
return Ordering::Greater;
}*/
a_path.partial_cmp(&b_path).unwrap()
}); // ensure we're actually installing them in the correct order
game_patches
.iter()
.map(|x| x.file_stem().unwrap().to_str().unwrap().to_string())
.collect()
}
/// Check if it's a valid patch client connecting
fn check_valid_patch_client(headers: &HeaderMap) -> bool {
let Some(user_agent) = headers.get("User-Agent") else {
return false;
};
user_agent == "FFXIV PATCH CLIENT"
}
async fn verify_session(
headers: HeaderMap,
Path((platform, channel, game_version, sid)): Path<(String, String, String, String)>,
body: String,
) -> impl IntoResponse {
if !check_valid_patch_client(&headers) {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
let config = get_config();
if !config.supports_platform(&platform) {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
tracing::info!("Verifying game components for {platform} {channel} {game_version} {body}...");
let body_parts: Vec<&str> = body.split('\n').collect();
let hashes = body_parts[0];
let expansion_versions = &body_parts[1..body_parts.len() - 1]; // last part is empty
let game_version = Version(&game_version);
let supported_expac_versions = get_supported_expac_versions();
for expansion_version in expansion_versions {
let expac_version_parts: Vec<&str> = expansion_version.split('\t').collect();
let expansion_name = expac_version_parts[0]; // e.g. ex1
let expansion_version = expac_version_parts[1];
if Version(expansion_version) > supported_expac_versions[expansion_name] {
tracing::warn!(
"{expansion_name} {expansion_version} is above supported version {}!",
supported_expac_versions[expansion_name]
);
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
}
// Their version is too new
if game_version > SUPPORTED_GAME_VERSION {
tracing::warn!("{game_version} is above supported game version {SUPPORTED_GAME_VERSION}!");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
// If we are up to date, yay!
if game_version == SUPPORTED_GAME_VERSION {
let mut headers = HeaderMap::new();
headers.insert("X-Patch-Unique-Id", sid.parse().unwrap());
return (headers).into_response();
}
let mut headers = HeaderMap::new();
headers.insert("X-Patch-Unique-Id", sid.parse().unwrap());
(headers).into_response()
}
async fn verify_boot(
headers: HeaderMap,
Path((platform, channel, boot_version)): Path<(String, String, String)>,
) -> impl IntoResponse {
if !check_valid_patch_client(&headers) {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
tracing::info!("Verifying boot components for {platform} {channel} {boot_version}...");
let config = get_config();
if !config.supports_platform(&platform) {
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
let actual_boot_version = boot_version.split("?time").collect::<Vec<&str>>()[0];
let boot_version = Version(actual_boot_version);
// If we are up to date, yay!
if boot_version == SUPPORTED_BOOT_VERSION {
let headers = HeaderMap::new();
return (headers).into_response();
}
// Their version is too new
if boot_version > SUPPORTED_BOOT_VERSION {
tracing::warn!("{boot_version} is above supported boot version {SUPPORTED_BOOT_VERSION}!");
return StatusCode::INTERNAL_SERVER_ERROR.into_response();
}
// check if we need any patching
let patches = list_patch_files(&config.patch.patches_location);
for patch in patches {
let patch_str: &str = &patch;
if actual_boot_version.partial_cmp(patch_str).unwrap() == Ordering::Less {
// not up to date!
let patch_list = PatchList {
id: "477D80B1_38BC_41d4_8B48_5273ADB89CAC".to_string(),
requested_version: boot_version.to_string().clone(),
patch_length: todo!(),
content_location: todo!(),
patches: vec![PatchEntry {
url: format!("http://{}/{}", config.patch.patch_dl_url, patch).to_string(),
version: "2023.09.15.0000.0000".to_string(),
hash_block_size: 50000000,
length: 1479062470,
size_on_disk: 0,
hashes: vec![],
unknown_a: 0,
unknown_b: 0,
}],
};
let patch_list_str = patch_list.to_string(PatchListType::Boot);
return patch_list_str.into_response();
}
}
let headers = HeaderMap::new();
(headers).into_response()
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new()
.route(
"/http/{platform}/{channel}/{game_version}/{sid}",
post(verify_session),
)
.route(
"/http/{platform}/{channel}/{boot_version}",
get(verify_boot),
);
let config = get_config();
let addr = config.patch.get_socketaddr();
tracing::info!("Patch server started on {addr}");
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}