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")); + } +}