From 1e343d0f101bbb305883b3b779ee1656fd1f6a61 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 23 Mar 2025 08:21:43 -0400 Subject: [PATCH] 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. --- src/bin/kawari-patch.rs | 58 ++++++++++++++++++++++++++++++-- src/config.rs | 2 +- src/lib.rs | 25 ++++++++++++++ src/patch/mod.rs | 2 ++ src/patch/version.rs | 74 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 src/patch/mod.rs create mode 100644 src/patch/version.rs diff --git a/src/bin/kawari-patch.rs b/src/bin/kawari-patch.rs index bc195fd..694ba6c 100644 --- a/src/bin/kawari-patch.rs +++ b/src/bin/kawari-patch.rs @@ -7,6 +7,8 @@ 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 { @@ -73,6 +75,7 @@ fn check_valid_patch_client(headers: &HeaderMap) -> bool { 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(); @@ -83,6 +86,45 @@ async fn verify_session( 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()); @@ -104,8 +146,20 @@ async fn verify_boot( return StatusCode::INTERNAL_SERVER_ERROR.into_response(); } - // Turns 2019.03.12.0000.0001/?time=2024-06-29-18-30 into just 2019.03.12.0000.0001 let actual_boot_version = boot_version.split("?time").collect::>()[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); @@ -115,7 +169,7 @@ async fn verify_boot( // not up to date! let patch_list = PatchList { id: "477D80B1_38BC_41d4_8B48_5273ADB89CAC".to_string(), - requested_version: boot_version.clone(), + requested_version: boot_version.to_string().clone(), patch_length: todo!(), content_location: todo!(), patches: vec![PatchEntry { diff --git a/src/config.rs b/src/config.rs index e8461c6..c4071a4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -122,7 +122,7 @@ pub struct PatchConfig { /// For example, "patch-dl.ffxiv.localhost". Patch files must be served so they're accessible as: "http://patch-dl.ffxiv.localhost/game/ex4/somepatchfilename.patch" pub patch_dl_url: String, /// Location of the patches directory on disk. Must be setup like so: - /// ``` + /// ```ignore /// (e.g. ffxivneo_release_game) / /// game/ /// ex1/ diff --git a/src/lib.rs b/src/lib.rs index b2fa6ae..b2748df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,9 @@ //! A server replacement for a certain MMO. +use std::collections::HashMap; + use minijinja::Environment; +use patch::Version; /// The blowfish implementation used for packet encryption. pub mod blowfish; @@ -26,6 +29,28 @@ pub mod packet; /// Logic server-specific code. pub mod login; +/// Patch server-specific code. +pub mod patch; + +/// Supported boot version. +pub const SUPPORTED_BOOT_VERSION: Version = Version("2025.01.10.0000.0001"); + +/// Supported game version. +pub const SUPPORTED_GAME_VERSION: Version = Version("2025.02.27.0000.0000"); + +const SUPPORTED_EXPAC_VERSIONS: [(&str, Version); 5] = [ + ("ex1", Version("2025.01.09.0000.0000")), + ("ex2", Version("2025.01.14.0000.0000")), + ("ex3", Version("2025.02.27.0000.0000")), + ("ex4", Version("2025.02.27.0000.0000")), + ("ex5", Version("2025.02.27.0000.0000")), +]; + +/// Supported expansion versions. +pub fn get_supported_expac_versions() -> HashMap<&'static str, Version<'static>> { + HashMap::from(SUPPORTED_EXPAC_VERSIONS) +} + pub fn setup_default_environment() -> Environment<'static> { let mut env = Environment::new(); env.add_template("admin.html", include_str!("../templates/admin.html")) diff --git a/src/patch/mod.rs b/src/patch/mod.rs new file mode 100644 index 0000000..1749726 --- /dev/null +++ b/src/patch/mod.rs @@ -0,0 +1,2 @@ +mod version; +pub use version::Version; diff --git a/src/patch/version.rs b/src/patch/version.rs new file mode 100644 index 0000000..c5ae258 --- /dev/null +++ b/src/patch/version.rs @@ -0,0 +1,74 @@ +use std::{ + cmp::Ordering, + fmt::{self, Display, Formatter}, +}; + +#[derive(PartialEq, Eq, PartialOrd)] +pub struct Version<'a>(pub &'a str); + +#[derive(PartialEq, Eq, Ord, PartialOrd)] +struct VersionParts { + year: i32, + month: i32, + day: i32, + patch1: i32, + patch2: i32, +} + +impl VersionParts { + fn new(version: &str) -> Self { + let parts: Vec<&str> = version.split('.').collect(); + + Self { + year: parts[0].parse::().unwrap(), + month: parts[1].parse::().unwrap(), + day: parts[2].parse::().unwrap(), + patch1: parts[3].parse::().unwrap(), + patch2: parts[4].parse::().unwrap(), + } + } +} + +impl Display for Version<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Ord for Version<'_> { + fn cmp(&self, other: &Self) -> Ordering { + let our_version_parts = VersionParts::new(self.0); + let their_version_parts = VersionParts::new(other.0); + + our_version_parts.cmp(&their_version_parts) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_eq() { + assert!(Version("2025.02.27.0000.0000") == Version("2025.02.27.0000.0000")); + assert!(Version("2025.01.20.0000.0000") != Version("2025.02.27.0000.0000")); + } + + #[test] + fn test_ordering() { + // year + assert!(Version("2025.02.27.0000.0000") > Version("2024.02.27.0000.0000")); + + // month + assert!(Version("2025.03.27.0000.0000") > Version("2025.02.27.0000.0000")); + + // day + assert!(Version("2025.02.28.0000.0000") > Version("2025.02.27.0000.0000")); + + // patch1 + assert!(Version("2025.02.27.1000.0000") > Version("2025.02.27.0000.0000")); + + // patch2 + assert!(Version("2025.02.27.0000.1000") > Version("2025.02.27.0000.0000")); + } +}