diff --git a/Cargo.toml b/Cargo.toml index c4105ed..109fd64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ prost = "0.8" bytes = "1.1.0" num-traits = "0.2" num-derive = "0.3" +base64 = "0.13.0" [build-dependencies] prost-build = { version = "0.8.0" } diff --git a/build.rs b/build.rs index 33fb7ad..565fc3f 100644 --- a/build.rs +++ b/build.rs @@ -4,6 +4,13 @@ fn main() -> Result<()> { let proto_dir = "protobuf"; let protos = vec![ + // Dispatch protocol + "QueryRegionListHttpRsp", + "QueryCurrRegionHttpRsp", + "RegionSimpleInfo", + "RegionInfo", + + // Game protocol "packet_header", "GetPlayerTokenReq", diff --git a/src/main.rs b/src/main.rs index ff8c2b7..4492613 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ #[macro_use] extern crate num_derive; +use std::thread; + mod server; mod utils; @@ -12,8 +14,14 @@ pub mod proto { } use server::NetworkServer; +use server::DispatchServer; fn main() { - let mut ns = NetworkServer::new("0.0.0.0", 9696).unwrap(); + thread::spawn(|| { + let mut ds = DispatchServer::new("127.0.0.1", 9696); + ds.run(); + }); + + let mut ns = NetworkServer::new("0.0.0.0", 4242).unwrap(); ns.run().expect("Failed to serve!"); } diff --git a/src/server/dispatch_server.rs b/src/server/dispatch_server.rs new file mode 100644 index 0000000..1215b91 --- /dev/null +++ b/src/server/dispatch_server.rs @@ -0,0 +1,258 @@ +use std::io::prelude::*; +use std::net::TcpListener; +use std::net::TcpStream; +use std::collections::HashMap; +use std::fs; + +use prost::Message; +extern crate base64; + +use crate::proto; +use mhycrypt; + +type RequestCallback = fn(slef: &DispatchServer, args: Vec<(&str, &str)>) -> (String, String); + +pub struct DispatchServer { + listener: TcpListener, + callbacks: HashMap, + ip: String, + port: u16, +} + +impl DispatchServer { + pub fn new(ip: &str, port: u16) -> DispatchServer { + let mut callbacks = HashMap::new(); + + let mut ds = DispatchServer { + listener: TcpListener::bind(format!("{}:{}", ip, port)).unwrap(), + callbacks: callbacks, + ip: ip.to_string(), + port: port, + }; + + ds.callbacks.insert("/query_region_list".to_string(), DispatchServer::query_region_list); + ds.callbacks.insert("/query_cur_region".to_string(), DispatchServer::query_cur_region); + ds.callbacks.insert("/account/risky/api/check".to_string(), DispatchServer::risky_api_check); + ds.callbacks.insert("/hk4e_global/mdk/shield/api/login".to_string(), DispatchServer::risky_api_check); + ds.callbacks.insert("/hk4e_global/combo/granter/login/v2/login".to_string(), DispatchServer::granter_login); + ds.callbacks.insert("/hk4e_global/mdk/shield/api/verify".to_string(), DispatchServer::shield_verify); + + return ds; + } + + pub fn run(&self) { + for stream in self.listener.incoming() { + let stream = stream.unwrap(); + + self.handle_connection(stream); + } + } + + fn handle_connection(&self, mut stream: TcpStream) { + let mut buffer = [0; 1024]; + + stream.read(&mut buffer).unwrap(); + + let buffer = String::from_utf8_lossy(&buffer); + + let data = buffer.split_whitespace().collect::>(); + let method = data[0]; + let url = data[1]; + + println!("Access to '{}' using {}", url, method); + + let (status_code, contents) = match method { + "GET" | "POST" => { + let parts = url.split('?').collect::>(); + let uri = parts[0]; + let params = if parts.len() > 1 { parts[1].split('&').collect::>() } else { Vec::new() }; + let params = params.into_iter().map(|x| x.split('=').collect::>()).filter(|x| x.len() > 1).map(|x| (x[0], x[1])).collect(); + + match self.callbacks.get(uri) { + Some(callback) => callback(&self, params), + None => ("404 Not Found".into(), "Nothing here".into()), + } + }, + _ => ("400 Bad Request".into(), "You are stupid".into()), + }; + + //let contents = fs::read_to_string(filename).unwrap(); + + let status_line = format!("HTTP/1.1 {}", status_code); + + let response = format!( + "{}\r\nContent-Length: {}\r\n\r\n{}", + status_line, + contents.len(), + contents + ); + + stream.write(response.as_bytes()).unwrap(); + stream.flush().unwrap(); + } + + fn query_region_list(&self, args: Vec<(&str, &str)>) -> (String, String) { + println!("Args: {:?}", args); + + let keys = self.load_keys("master"); + + let mut region_info = proto::RegionSimpleInfo::default(); + region_info.name = "private_server".into(); + region_info.title = "Private Server".into(); + region_info.r#type = "DEV_PUBLIC".into(); + region_info.dispatch_url = format!("http://localhost:{}/query_cur_region", self.port); + + let mut region_list = proto::QueryRegionListHttpRsp::default(); + region_list.region_list = vec![region_info]; + region_list.enable_login_pc = true; + + region_list.client_secret_key = keys.0.clone(); + + let json_config = "{\"sdkenv\":\"2\",\"checkdevice\":\"false\",\"loadPatch\":\"false\",\"showexception\":\"false\",\"regionConfig\":\"pm|fk|add\",\"downloadMode\":\"0\"}"; + + let mut custom_config = json_config.as_bytes().to_owned(); + + mhycrypt::mhy_xor(&mut custom_config, &keys.1); + + region_list.client_custom_config_encrypted = custom_config.to_vec(); + + let mut region_list_buf = Vec::new(); + + region_list.encode(&mut region_list_buf).unwrap(); + + return ("200 OK".into(), base64::encode(region_list_buf)); + } + + fn query_cur_region(&self, args: Vec<(&str, &str)>) -> (String, String) { + println!("Args: {:?}", args); + + let keys = self.load_keys("master"); + + let mut region_info = proto::RegionInfo::default(); + region_info.gateserver_ip = "127.0.0.1".to_string(); + region_info.gateserver_port = 4242; + region_info.secret_key = keys.0.clone(); + + let mut region_config = proto::QueryCurrRegionHttpRsp::default(); + region_config.region_info = Some(region_info); + region_config.client_secret_key = keys.0.clone(); + + let json_config = format!("{{\"coverSwitch\": [\"8\"], \"perf_report_config_url\": \"http://localhost:{}/config/verify\", \"perf_report_record_url\": \"http://localhost:{}/dataUpload\" }}", + self.port, self.port); + + let mut custom_config = json_config.as_bytes().to_owned(); + + mhycrypt::mhy_xor(&mut custom_config, &keys.1); + + region_config.region_custom_config_encrypted = custom_config.to_vec(); + + let mut region_conf_buf = Vec::new(); + + region_config.encode(&mut region_conf_buf).unwrap(); + + return ("200 OK".into(), base64::encode(region_conf_buf)); + } + + fn risky_api_check(&self, args: Vec<(&str, &str)>) -> (String, String) { + let email = "ceo@hoyolab.com"; + let name = "Ceo"; + let token = "Fake-token-hahaha"; + let uid = 0x1234; + + let payload = self.build_account_data(email, name, token, uid); + + return ("200 OK".into(), self.make_answer(0, &payload)); + } + + fn granter_login(&self, args: Vec<(&str, &str)>) -> (String, String) { + let payload = self.verify_token_v2(); + + return ("200 OK".into(), self.make_answer(0, &payload)); + } + + fn shield_verify(&self, args: Vec<(&str, &str)>) -> (String, String) { + let email = "ceo@hoyolab.com"; + let name = "Ceo"; + let token = "Fake-token-hahaha"; + let uid = 0x1234; + + let payload = self.build_account_data(email, name, token, uid); + + return ("200 OK".into(), self.make_answer(0, &payload)); + } + + fn load_keys(&self, name: &str) -> (Vec, Vec) { + // Key + let filename = format!("./{}/{}.key", "keys", name); + let mut f = fs::File::open(&filename).expect(&format!("File '{}' not found", filename)); + let metadata = fs::metadata(&filename).expect("unable to read metadata"); + let mut key = vec![0; metadata.len() as usize]; + f.read(&mut key).expect("buffer overflow"); + // Ec2b + let filename = format!("./{}/{}.ec2b", "keys", name); + let mut f = fs::File::open(&filename).expect(&format!("File '{}' not found", filename)); + let metadata = fs::metadata(&filename).expect("unable to read metadata"); + let mut ec2b = vec![0; metadata.len() as usize]; + f.read(&mut ec2b).expect("buffer overflow"); + return (ec2b, key); + } + + fn verify_token_v2(&self) -> String { + let account_type = 1; + let combo_id = 0x4321; + let combo_token = "Fake-token-hehehe"; + let open_id = 0x1234; + + return format!("{{ + \"account_type\": \"{}\", + \"combo_id\": \"{}\", + \"combo_token\": \"{}\", + \"data\": {{\"guest\": \"false\"}}, + \"heartbeat\": \"false\", + \"open_id\": \"{}\" + }}", account_type, combo_id, combo_token, open_id); + } + + fn build_account_data(&self, email: &str, name: &str, token: &str, uid: i32) -> String { + let payload = format!("{{ + \"account\": {{ + \"apple_name\": \"\", + \"country\": \"\", + \"email\": \"{}\", + \"facebook_name\": \"\", + \"game_center_name\": \"\", + \"google_name\": \"\", + \"identity_card\": \"\", + \"is_email_verify\": \"0\", + \"mobile\": \"\", + \"name\": \"{}\", + \"realname\": \"\", + \"safe_mobile\": \"\", + \"sony_name\": \"\", + \"tap_name\": \"\", + \"token\": \"{}\", + \"twitter_name\": \"\", + \"uid\": \"{}\" + }}, + \"device_grant_required\": \"false\", + \"realperson_required\": \"false\", + \"safe_moblie_required\": \"false\" + }}", email, name, token, uid); + + return payload.into(); + } + + fn make_answer(&self, code: i32, data: &str) -> String { + let message = match code { + 0 => "OK", + -1 => "not matched", + _ => "ERROR", + }; + + return format!("{{ + \"retcode\": \"{}\", + \"message\": \"{}\", + \"data\": {} + }}", code, message, data).to_string(); + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index 86f8335..4d73c51 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -4,6 +4,7 @@ mod game_world; mod auth_manager; mod client_connection; mod ipc_message; +mod dispatch_server; pub use self::network_server::NetworkServer; pub use self::game_server::GameServer; @@ -11,3 +12,4 @@ pub use self::game_world::GameWorld; pub use self::auth_manager::AuthManager; pub use self::client_connection::ClientConnection; pub use self::ipc_message::IpcMessage; +pub use self::dispatch_server::DispatchServer;