From 5c6a2ac53f6f410e51506a1a4759903860880aae Mon Sep 17 00:00:00 2001 From: J-onasJones Date: Thu, 4 Jan 2024 00:48:38 +0100 Subject: [PATCH] Added mcmod update branch --- Cargo.lock | 39 +++++ Cargo.toml | 1 + src/v1/updates/minecraft/mods/mod.rs | 151 +++++++++++++++--- .../updates/minecraft/mods/telemetry/mod.rs | 46 ++++++ 4 files changed, 219 insertions(+), 18 deletions(-) create mode 100644 src/v1/updates/minecraft/mods/telemetry/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 5a8ba3e..3d51ac3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -527,6 +536,7 @@ dependencies = [ "lastfm", "log", "parking_lot", + "regex", "reqwest", "serde", "serde_json", @@ -886,6 +896,35 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "reqwest" version = "0.11.22" diff --git a/Cargo.toml b/Cargo.toml index 81222a9..0d5ca30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ chrono = "0.4.31" toml = "0.8.8" reqwest = { version = "0.11.22", features = ["json"] } serde_json = "1.0.108" +regex = "1" diff --git a/src/v1/updates/minecraft/mods/mod.rs b/src/v1/updates/minecraft/mods/mod.rs index 1959f92..6f2faaf 100644 --- a/src/v1/updates/minecraft/mods/mod.rs +++ b/src/v1/updates/minecraft/mods/mod.rs @@ -1,29 +1,144 @@ +use std::collections::HashMap; +use std::net::IpAddr; +use reqwest::Error; +use serde_json::json; +use serde::{Deserialize, Serialize}; use warp::Filter; +use regex::Regex; + +use crate::error_responses::BadRequestError; pub fn get_mods_paths() -> impl warp::Filter + Clone { // any path that starts with /v1/updates/minecraft/mods/{modname}/{loadername}/{version} calls handle_path warp::path("v1").and(warp::path("updates")).and(warp::path("minecraft")).and(warp::path("mods")) - .and(warp::path::param()) - .and(warp::path::param()) - .and(warp::path::param()) - .and(warp::path::end()) - .and(warp::addr::remote()) - .map(handle_path) + .and(warp::get().and(warp::path::param()).and(warp::path::param()).and(warp::path::param()).and(warp::path::param()).and(warp::path::end()).and(warp::filters::header::headers_cloned()).and(warp::query::>()).and_then(handle_path)) } -fn handle_path(modname: String, loadername: String, version: String, remote_ip: Option) -> String { - format!("modname: {}, loadername: {}, version: {}, IP: {}", modname, loadername, version, remote_ip.unwrap_or(std::net::SocketAddr::from(([0, 0, 0, 0], 0))).ip()) +#[derive(Debug, Deserialize, Serialize)] +struct ModData { + package: String, + name: String, + versions: Vec>>, } -// fn handle_with_headers( -// headers: warp::http::HeaderMap, -// ) -> String { -// // Iterate through the headers and print them -// for (name, value) in headers.iter() { -// println!("Header: {}: {}", name, value.to_str().unwrap_or("Invalid UTF-8")); -// } +#[derive(Debug, Deserialize, Serialize, Clone)] +struct ModVersion { + recommended: String, + latest: String, + all: Vec, +} -// // Respond with a message or perform other actions as needed -// "Headers received".to_string() -// } +// get json data from https://https://cdn.jonasjones.dev/api/mcmods/mcmod_metadata.json +pub async fn fetch_data() -> Result { + let url = "https://cdn.jonasjones.dev/api/mcmods/mcmod_metadata.json"; + let response = reqwest::get(url).await?; + + if response.status().is_success() { + // Parse the JSON response + let json_data: serde_json::Value = response.json().await?; + return Ok(json_data); + } else { + // Handle non-successful status codes + Err(response.error_for_status().unwrap_err()) + } +} + +fn is_valid_ip(ip_str: &str) -> bool { + if let Ok(ip) = ip_str.parse::() { + match ip { + IpAddr::V4(_) => true, + IpAddr::V6(_) => true, + } + } else { + false + } +} + +fn is_valid_minecraft_version(version: &str) -> bool { + // Define the regex pattern for the Minecraft version + let pattern = Regex::new(r"^1\.\d{1,2}(\.\d)?$").unwrap(); + + // Check if the provided version matches the pattern + pattern.is_match(version) +} + +fn get_header_forward_for_ip(headers: warp::http::HeaderMap) -> String { + // check if the header X-Forward-For exists and return the ip, if not, return an empty string + if let Some(forwarded_for) = headers.get("X-Forwarded-For") { + if let Ok(ip) = forwarded_for.to_str() { + // Extract the first IP address from the comma-separated list + if let Some(first_ip) = ip.split(',').next() { + return first_ip.trim().to_string(); + } + } + } + String::new() +} + +async fn handle_path(modpackage: String, loadername: String, mcversion: String, modversion: String, headers: warp::http::HeaderMap, params: HashMap) -> Result { + // Retrieve the IP from the header and check if it's valid + let mut client_ip = get_header_forward_for_ip(headers); + if !is_valid_ip(&client_ip) { + client_ip = params.get("ip").unwrap_or(&"".to_string()).to_string(); + if !is_valid_ip(&client_ip) { + client_ip = "Not valid".to_string(); + } + } + + // check if the minecraft version is valid + if !is_valid_minecraft_version(&mcversion) { + return Err(warp::reject::custom(BadRequestError)); + } + + // fetch the data + let data = fetch_data().await.unwrap(); + + // filter the data + // convert the raw list of data into a list of ModData and ModVersion + let mods_data: Vec = serde_json::from_value(data).unwrap(); + + // get the mod data from the requested mod + let mod_data: ModData = mods_data.into_iter().find(|mod_data| mod_data.package == modpackage).unwrap(); + + + // get the version data from the requested loader and remove the other loaders + let version_data: HashMap = mod_data.versions.into_iter().find(|version_data| version_data.contains_key(&loadername)).unwrap().remove(&loadername).unwrap(); + + // turn version_data into an object of String: ModVersion key value pairs + let version_data: HashMap = version_data.into_iter().map(|(key, value)| (key, value)).collect(); + + // get the version data for the current minecraft version + let version_data: ModVersion = version_data.get(&mcversion).unwrap().clone(); + + // get recommended and latest version + let recommended_version = version_data.recommended.clone(); + let latest_version = version_data.latest.clone(); + + // determine whether the client is up to date + let mut up_to_date = false; + if modversion == recommended_version { + up_to_date = true; + } + + // determine if telemetry is enabled by checking if the client_ip is valid + let mut telemetry = false; + if is_valid_ip(&client_ip) { + telemetry = true; + } + + // create the response + let response = json!({ + "promos": { + "latest": latest_version, + "recommended": recommended_version + }, + "upToDate": up_to_date, + "telemetry_enabled": telemetry + }); + + //TODO: Add way to process telemetry data + + // return the data + return Ok(warp::reply::json(&response)); +} diff --git a/src/v1/updates/minecraft/mods/telemetry/mod.rs b/src/v1/updates/minecraft/mods/telemetry/mod.rs new file mode 100644 index 0000000..dae1d5c --- /dev/null +++ b/src/v1/updates/minecraft/mods/telemetry/mod.rs @@ -0,0 +1,46 @@ +use std::fs::OpenOptions; +use std::io::{self, Write}; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; +use std::path::Path; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct IpInfo { + region: String, + // Add other fields as needed +} + +fn get_ip_hash(ip: &str) -> u64 { + let mut hasher = DefaultHasher::new(); + ip.hash(&mut hasher); + hasher.finish() +} + +pub fn log_ip_info(ip_address: &str, file_path: &str, mod_package: &str) -> io::Result<()> { + let ip_hash = get_ip_hash(ip_address); + + let ip_info = match get_ip_info(ip_address) { + Ok(info) => info, + Err(err) => { + IpInfo { region: "Unknown".to_string() } // Default to "Unknown" in case of an error + } + }; + + let mut file = OpenOptions::new() + .create(true) + .append(true) + .open(file_path)?; + + writeln!(file, "{} {} {}", ip_hash, ip_info.region, mod_package)?; + + Ok(()) +} + +fn main() { + let file_path = "ip_log.txt"; // Replace with your desired file path + + // Example usage + match log_ip_info("8.8.8.8", file_path) { + Err(err) => eprintln!("Error: {}", err), + } +}