1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-25 08:27:44 +00:00

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.
This commit is contained in:
Joshua Goins 2025-03-23 08:21:43 -04:00
parent 65500d15ad
commit 1e343d0f10
5 changed files with 158 additions and 3 deletions

View file

@ -7,6 +7,8 @@ use axum::response::IntoResponse;
use axum::routing::post; use axum::routing::post;
use axum::{Router, routing::get}; use axum::{Router, routing::get};
use kawari::config::get_config; 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}; use physis::patchlist::{PatchEntry, PatchList, PatchListType};
fn list_patch_files(dir_path: &str) -> Vec<String> { fn list_patch_files(dir_path: &str) -> Vec<String> {
@ -73,6 +75,7 @@ fn check_valid_patch_client(headers: &HeaderMap) -> bool {
async fn verify_session( async fn verify_session(
headers: HeaderMap, headers: HeaderMap,
Path((platform, channel, game_version, sid)): Path<(String, String, String, String)>, Path((platform, channel, game_version, sid)): Path<(String, String, String, String)>,
body: String,
) -> impl IntoResponse { ) -> impl IntoResponse {
if !check_valid_patch_client(&headers) { if !check_valid_patch_client(&headers) {
return StatusCode::INTERNAL_SERVER_ERROR.into_response(); return StatusCode::INTERNAL_SERVER_ERROR.into_response();
@ -83,6 +86,45 @@ async fn verify_session(
return StatusCode::INTERNAL_SERVER_ERROR.into_response(); 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(); let mut headers = HeaderMap::new();
headers.insert("X-Patch-Unique-Id", sid.parse().unwrap()); headers.insert("X-Patch-Unique-Id", sid.parse().unwrap());
@ -104,8 +146,20 @@ async fn verify_boot(
return StatusCode::INTERNAL_SERVER_ERROR.into_response(); 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::<Vec<&str>>()[0]; 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 // check if we need any patching
let patches = list_patch_files(&config.patch.patches_location); let patches = list_patch_files(&config.patch.patches_location);
@ -115,7 +169,7 @@ async fn verify_boot(
// not up to date! // not up to date!
let patch_list = PatchList { let patch_list = PatchList {
id: "477D80B1_38BC_41d4_8B48_5273ADB89CAC".to_string(), id: "477D80B1_38BC_41d4_8B48_5273ADB89CAC".to_string(),
requested_version: boot_version.clone(), requested_version: boot_version.to_string().clone(),
patch_length: todo!(), patch_length: todo!(),
content_location: todo!(), content_location: todo!(),
patches: vec![PatchEntry { patches: vec![PatchEntry {

View file

@ -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" /// 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, pub patch_dl_url: String,
/// Location of the patches directory on disk. Must be setup like so: /// Location of the patches directory on disk. Must be setup like so:
/// ``` /// ```ignore
/// <channel> (e.g. ffxivneo_release_game) / /// <channel> (e.g. ffxivneo_release_game) /
/// game/ /// game/
/// ex1/ /// ex1/

View file

@ -1,6 +1,9 @@
//! A server replacement for a certain MMO. //! A server replacement for a certain MMO.
use std::collections::HashMap;
use minijinja::Environment; use minijinja::Environment;
use patch::Version;
/// The blowfish implementation used for packet encryption. /// The blowfish implementation used for packet encryption.
pub mod blowfish; pub mod blowfish;
@ -26,6 +29,28 @@ pub mod packet;
/// Logic server-specific code. /// Logic server-specific code.
pub mod login; 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> { pub fn setup_default_environment() -> Environment<'static> {
let mut env = Environment::new(); let mut env = Environment::new();
env.add_template("admin.html", include_str!("../templates/admin.html")) env.add_template("admin.html", include_str!("../templates/admin.html"))

2
src/patch/mod.rs Normal file
View file

@ -0,0 +1,2 @@
mod version;
pub use version::Version;

74
src/patch/version.rs Normal file
View file

@ -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::<i32>().unwrap(),
month: parts[1].parse::<i32>().unwrap(),
day: parts[2].parse::<i32>().unwrap(),
patch1: parts[3].parse::<i32>().unwrap(),
patch2: parts[4].parse::<i32>().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"));
}
}