diff --git a/src/server/dispatch_server.rs b/src/server/dispatch_server.rs index 7a7be17..c1b3c36 100644 --- a/src/server/dispatch_server.rs +++ b/src/server/dispatch_server.rs @@ -10,19 +10,22 @@ extern crate base64; extern crate actix_web; extern crate openssl; -use serde::{Deserialize,Serialize}; +use serde::{de, Deserialize, Deserializer, Serialize}; use futures::executor; use actix_web::{rt::System, web, get, App, HttpRequest, HttpResponse, HttpServer, Responder, middleware::Logger}; use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslVerifyMode, SslOptions, SslMode}; +use rand::{distributions::Alphanumeric, Rng}; use prost::Message; use mhycrypt; +use pretty_env_logger::env_logger::fmt; +use serde::de::Unexpected; +//use openssl::rand; #[derive(Clone)] -pub struct DispatchServer { -} +pub struct DispatchServer {} #[derive(Deserialize,Debug)] struct ClientInfo { @@ -57,7 +60,7 @@ struct LoginData { is_crypto: bool, password: String, } - +/* #[derive(Deserialize,Debug)] struct GranterData { app_id: String, @@ -65,6 +68,70 @@ struct GranterData { device: String, sign: String, data: String, +}*/ + +#[derive(Deserialize,Debug)] +struct GranterData { + #[serde(deserialize_with = "deserialize_u32_or_string")] + app_id: u32, + #[serde(deserialize_with = "deserialize_u32_or_string")] + channel_id: u32, + device: String, + sign: String, + data: String, +} + +/* Deserialization hack */ +fn deserialize_u32_or_string<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum StrOrU32<'a> { + Str(&'a str), + U32(u32), + } + + Ok(match StrOrU32::deserialize(deserializer)? { + StrOrU32::Str(v) => v.parse().unwrap(), // Ignoring parsing errors + StrOrU32::U32(v) => v, + }) +} + +#[derive(Deserialize,Debug)] +struct MinorApiLogData { + data: String, +} + +#[derive(Deserialize,Debug)] +struct GeetestGetData { + gt: String, + challenge: String, + lang: String, + is_next: Option, + client_type: Option, + w: Option, + pt: Option, + callback: Option, +} + +#[derive(Deserialize,Debug)] +struct GeetestGetTypeData { + gt: String, + t: u64, + callback: Option, +} + +#[derive(Deserialize,Debug)] +struct GeetestAjaxData { + gt: String, + challenge: String, + client_type: Option, + w: Option, + callback: Option, + #[serde(rename = "$_BBF")] + BBF: Option, } impl DispatchServer { @@ -84,45 +151,74 @@ impl DispatchServer { async fn run_internal(self: &Arc) { //let (http_port, https_port) = (2880, 2443); + println!("Hostname {}, local IP {}", DispatchServer::get_hostname(), DispatchServer::get_local_ip()); + let (http_port, https_port) = (80, 443); let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls_server()).unwrap(); - builder.set_verify(SslVerifyMode::NONE); - builder.set_min_proto_version(None).unwrap(); - builder.set_cipher_list("DEFAULT").unwrap(); - builder.set_mode(SslMode::NO_AUTO_CHAIN | SslMode::SEND_FALLBACK_SCSV); + //builder.set_verify(SslVerifyMode::NONE); + //builder.set_min_proto_version(None).unwrap(); + //builder.set_cipher_list("DEFAULT").unwrap(); + //builder.set_mode(SslMode::NO_AUTO_CHAIN | SslMode::SEND_FALLBACK_SCSV); builder.set_private_key_file("keys/ssl.key", SslFiletype::PEM).unwrap(); builder.set_certificate_chain_file("keys/ssl.cer").unwrap(); let http_server = HttpServer::new(move || { App::new() - .wrap(Logger::default()) - .route("/", web::get().to(|| HttpResponse::Ok())) - .route("/query_region_list", web::get().to(DispatchServer::query_region_list)) - .route("/query_cur_region", web::get().to(DispatchServer::query_cur_region)) - //.route("", web::post().to(DispatchServer::)) - .route("/hk4e_global/mdk/shield/api/verify", web::post().to(DispatchServer::shield_verify)) - .route("/account/risky/api/check", web::post().to(DispatchServer::risky_api_check)) - .route("/hk4e_global/mdk/shield/api/login", web::post().to(DispatchServer::shield_login)) - .route("/hk4e_global/combo/granter/login/v2/login", web::post().to(DispatchServer::granter_login)) + .wrap(Logger::default()) + .route("/", web::get().to(|| HttpResponse::Ok())) + .route("/query_security_file", web::get().to(DispatchServer::query_security_file)) + .route("/query_region_list", web::get().to(DispatchServer::query_region_list)) + .route("/query_cur_region", web::get().to(DispatchServer::query_cur_region)) + //.route("", web::post().to(DispatchServer::)) + .route("/hk4e_global/mdk/shield/api/verify", web::post().to(DispatchServer::shield_verify)) + //.route("/account/risky/api/check", web::post().to(DispatchServer::risky_api_check)) + .route("/account/risky/api/check", web::post().to(DispatchServer::risky_api_check_old)) + .route("/hk4e_global/mdk/shield/api/login", web::post().to(DispatchServer::shield_login)) + .route("/hk4e_global/combo/granter/login/v2/login", web::post().to(DispatchServer::granter_login)) + // Misc stuff, not really required + .route("/common/h5log/log/batch", web::post().to(DispatchServer::minor_api_log)) + + .route("/combo/box/api/config/sdk/combo", web::get().to(DispatchServer::combo_combo)) + .route("/hk4e_global/combo/granter/api/getConfig", web::get().to(DispatchServer::get_config)) + .route("/hk4e_global/mdk/shield/api/loadConfig", web::get().to(DispatchServer::load_config)) + //.route("/hk4e_global/combo/granter/api/getFont", web::get().to(DispatchServer::get_font)) + .route("/hk4e_global/mdk/agreement/api/getAgreementInfos", web::get().to(DispatchServer::get_agreement_infos)) + .route("/admin/mi18n/plat_oversea/m2020030410/m2020030410-version.json", web::get().to(DispatchServer::version_data)) + .route("/hk4e_global/combo/granter/api/compareProtocolVersion", web::post().to(DispatchServer::compare_protocol_version)) + // GEETEST + .route("/get.php", web::get().to(DispatchServer::geetest_get)) + .route("/gettype.php", web::get().to(DispatchServer::geetest_get_type)) + .route("/ajax.php", web::get().to(DispatchServer::geetest_ajax_get)) + .route("/ajax.php", web::post().to(DispatchServer::geetest_ajax_post)) + // Logging + .route("/log/sdk/upload", web::post().to(DispatchServer::redirect_log_to_ccp)) + .route("/sdk/dataUpload", web::post().to(DispatchServer::log_skip)) + .route("/crash/dataUpload", web::post().to(DispatchServer::log_skip)) + }) - .bind(format!("127.0.0.1:{}", http_port)).expect("Failed to bind HTTP port") - .bind_openssl(format!("127.0.0.1:{}", https_port), builder).expect("Failed to bind HTTPS port") + .bind(format!("0.0.0.0:{}", http_port)).expect("Failed to bind HTTP port") + .bind_openssl(format!("0.0.0.0:{}", https_port), builder).expect("Failed to bind HTTPS port") .run(); http_server.stop(true).await; } + async fn query_security_file() -> String { + return "".to_string(); + } + async fn query_region_list(c: web::Query) -> String { println!("Client: {:?}", c); let keys = DispatchServer::load_keys("master"); let mut region_info = proto::RegionSimpleInfo::default(); - region_info.name = "private_server".into(); - region_info.title = "Private Server".into(); + region_info.name = "ps_rusty".into(); + region_info.title = "Rusty Samovar".into(); region_info.r#type = "DEV_PUBLIC".into(); - region_info.dispatch_url = format!("http://localhost:{}/query_cur_region", 80); + //region_info.dispatch_url = format!("http://{}:{}/query_cur_region", DispatchServer::get_hostname(), 80); + region_info.dispatch_url = format!("http://127.0.0.1:{}/query_cur_region", 80); let mut region_list = proto::QueryRegionListHttpRsp::default(); region_list.region_list = vec![region_info]; @@ -151,7 +247,7 @@ impl DispatchServer { let keys = DispatchServer::load_keys("master"); let mut region_info = proto::RegionInfo::default(); - region_info.gateserver_ip = "127.0.0.1".to_string(); + region_info.gateserver_ip = DispatchServer::get_local_ip(); region_info.gateserver_port = 4242; region_info.secret_key = keys.0.clone(); @@ -159,8 +255,8 @@ impl DispatchServer { 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\" }}", - 80, 80); + let json_config = format!("{{\"coverSwitch\": [\"8\"], \"perf_report_config_url\": \"http://{}:{}/config/verify\", \"perf_report_record_url\": \"http://{}:{}/dataUpload\" }}", + DispatchServer::get_hostname(), 80, DispatchServer::get_hostname(), 80); let mut custom_config = json_config.as_bytes().to_owned(); @@ -175,15 +271,38 @@ impl DispatchServer { return base64::encode(region_conf_buf); } - async fn risky_api_check(a: web::Json) -> String { + async fn risky_api_check_old(a: web::Json) -> String { println!("Action: {:?}", a); let email = "ceo@hoyolab.com"; let name = "Ceo"; - let token = "Fake-token-hahaha"; + let token = Self::generate_fake_token(); let uid = 0x1234; - let payload = DispatchServer::build_account_data(email, name, token, uid); + let payload = DispatchServer::build_account_data(email, name, &token, uid); + + return DispatchServer::make_answer(0, &payload); + } + + async fn risky_api_check(a: web::Json) -> String { + println!("Action: {:?}", a); + + let challenge = "5876e8bb6d90e0d6cf4dd26b109fe508"; + let gt = "16bddce04c7385dbb7282778c29bba3e"; + let id = "a0f5968aa4664b55ac914bffa1cd8058"; + + let payload = format!(" + {{ + \"action\": \"ACTION_GEETEST\", + \"geetest\": {{ + \"challenge\": \"{}\", + \"gt\": \"{}\", + \"new_captcha\": 1, + \"success\": 1 + }}, + \"id\": \"{}\" + }} + ", challenge, gt, id); return DispatchServer::make_answer(0, &payload); } @@ -193,10 +312,10 @@ impl DispatchServer { let email = "ceo@hoyolab.com"; let name = "Ceo"; - let token = "Fake-token-hahaha"; + let token = Self::generate_fake_token(); let uid = 0x1234; - let payload = DispatchServer::build_account_data(email, name, token, uid); + let payload = DispatchServer::build_account_data(email, name, &token, uid); return DispatchServer::make_answer(0, &payload); } @@ -209,19 +328,312 @@ impl DispatchServer { return DispatchServer::make_answer(0, &payload); } + async fn combo_combo() -> String { + + let payload = format!("{{ + \"vals\": {{ + \"disable_email_bind_skip\": \"false\", + \"email_bind_remind\": \"true\", + \"email_bind_remind_interval\": \"7\" + }} + }}"); + + return DispatchServer::make_answer(0,&payload); + } + + async fn get_config() -> String { + let payload = format!("{{ + \"announce_url\": \"https://localhost/hk4e/announcement/index.html\", + \"disable_ysdk_guard\": false, + \"enable_announce_pic_popup\": true, + \"log_level\": \"INFO\", + \"protocol\": true, + \"push_alias_type\": 2, + \"qr_enabled\": false + }}"); + + return DispatchServer::make_answer(0,&payload); + } + + async fn load_config() -> String { + let payload = format!("{{ + \"client\": \"PC\", + \"disable_mmt\": false, + \"disable_regist\": false, + \"enable_email_captcha\": false, + \"enable_ps_bind_account\": false, + \"game_key\": \"hk4e_global\", + \"guest\": false, + \"id\": 6, + \"identity\": \"I_IDENTITY\", + \"ignore_versions\": \"\", + \"name\": \"原神海外\", + \"scene\": \"S_NORMAL\", + \"server_guest\": false, + \"thirdparty\": [ + \"fb\", + \"tw\" + ], + \"thirdparty_ignore\": {{ + \"fb\": \"\", + \"tw\": \"\" + }} + }}"); + return DispatchServer::make_answer(0,&payload); + } + async fn shield_verify(t: web::Json) -> String { println!("Token: {:?}", t); let email = "ceo@hoyolab.com"; let name = "Ceo"; - let token = "Fake-token-hahaha"; + let token = t.token.clone(); let uid = t.uid.parse().unwrap(); - let payload = DispatchServer::build_account_data(email, name, token, uid); + let payload = DispatchServer::build_account_data(email, name, &token, uid); return DispatchServer::make_answer(0, &payload); } + async fn minor_api_log(l: web::Json) -> String { + return "{\"retcode\":0,\"message\":\"success\",\"data\":null}".to_string(); + } + + /* + GEETEST + */ + async fn geetest_get(g: web::Query) -> String { + println!("GeetestGet: {:?}", g); + + let is_next = match g.is_next { + None => false, + Some(_) => true, + }; + + if (is_next) { + let callback = g.callback.as_ref().unwrap(); + + return format!(" + {}( {{ + \"gt\": \"{}\", + \"challenge\": \"{}\", + \"id\": \"a7b56e21f6771ab10e2bc4a3a511c4be0\", + \"bg\": \"pictures/gt/1dce8a0cd/bg/744f986a0.jpg\", + \"fullbg\": \"pictures/gt/1dce8a0cd/1dce8a0cd.jpg\", + \"link\": \"\", + \"ypos\": 85, + \"xpos\": 0, + \"height\": 160, + \"slice\": \"pictures/gt/1dce8a0cd/slice/744f986a0.png\", \ + \"api_server\": \"https://api-na.geetest.com/\", + \"static_servers\": [\"static.geetest.com/\", \"dn-staticdown.qbox.me/\"], + \"mobile\": true, + \"theme\": \"ant\", + \"theme_version\": \"1.2.6\", + \"template\": \"\", + \"logo\": false, + \"clean\": false, + \"type\": \"multilink\", + \"fullpage\": false, + \"feedback\": \"\", + \"show_delay\": 250, + \"hide_delay\": 800, + \"benchmark\": false, + \"version\": \"6.0.9\", + \"product\": \"embed\", + \"https\": true, + \"width\": \"100%\", + \"c\": [12, 58, 98, 36, 43, 95, 62, 15, 12], + \"s\": \"6b70592c\", + \"so\": 0, + \"i18n_labels\": {{ + \"cancel\": \"Cancel\", + \"close\": \"Close\", + \"error\": \"Error. Close and retry.\", + \"fail\": \"Incorrect position\", + \"feedback\": \"Info\", + \"forbidden\": \"Retry after 3 seconds\", + \"loading\": \"Loading\", + \"logo\": \"Geetest\", + \"read_reversed\": false, + \"refresh\": \"Refresh\", + \"slide\": \"Slide to unlock\", + \"success\": \"sec s. You're better than score% of users\", + \"tip\": \"\", + \"voice\": \"Voice test\" + }}, + \"gct_path\": \"/static/js/gct.d0a2919ae56f007ecb8e22fb47f80f33.js\" + }} )", callback, g.gt, g.challenge); + } else { + let data = " + ( { + \"status\": \"success\", + \"data\": { + \"theme\": \"wind\", + \"theme_version\": \"1.5.8\", + \"static_servers\": [\"static.geetest.com\", \"dn-staticdown.qbox.me\"], + \"api_server\": \"api-na.geetest.com\", + \"logo\": false, + \"feedback\": \"\", + \"c\": [12, 58, 98, 36, 43, 95, 62, 15, 12], + \"s\": \"3f6b3542\", + \"i18n_labels\": { + \"copyright\": \"Geetest\", + \"error\": \"Error\", + \"error_content\": \"Retry\", + \"error_title\": \"Timeout\", + \"fullpage\": \"Confirm\", + \"goto_cancel\": \"Cancel\", + \"goto_confirm\": \"OK\", + \"goto_homepage\": \"Go to Geetest homepage?\", + \"loading_content\": \"Confirm\", + \"next\": \"Loaging\", + \"next_ready\": \"Not fulfilled\", + \"read_reversed\": false, + \"ready\": \"Click to confirm\", + \"refresh_page\": \"Error. Refresh the page to continue.\", + \"reset\": \"Retry\", + \"success\": \"Success\", + \"success_title\": \"Success\" + } + } + }) + "; + + return match g.callback.as_ref() { + None => data.to_string(), + Some(callback) => format!( + "{}{}", + callback, data), + } + } + } + + async fn geetest_get_type(gt: web::Query) -> String { + println!("GeetestGetType: {:?}", gt); + + let data = "\ + ( { + \"status\": \"success\", + \"data\": { + \"type\": \"fullpage\", + \"static_servers\": [\"static.geetest.com/\", \"dn-staticdown.qbox.me/\"], + \"click\": \"/static/js/click.3.0.2.js\", + \"pencil\": \"/static/js/pencil.1.0.3.js\", + \"voice\": \"/static/js/voice.1.2.0.js\", + \"fullpage\": \"/static/js/fullpage.9.0.8.js\", + \"beeline\": \"/static/js/beeline.1.0.1.js\", + \"slide\": \"/static/js/slide.7.8.6.js\", + \"geetest\": \"/static/js/geetest.6.0.9.js\", + \"aspect_radio\": { + \"slide\": 103, \"click\": 128, \"voice\": 128, \"pencil\": 128, \"beeline\": 50 + } + } + }) + "; + + return match >.callback { + None => data.to_string(), + Some(callback) => format!( + "{}{}", + callback, data), + }; + } + + async fn geetest_ajax_get(ga: web::Query) -> String { + return Self::geetest_ajax(ga.into_inner()).await; + } + + async fn geetest_ajax_post(ga: web::Json) -> String { + return Self::geetest_ajax(ga.into_inner()).await; + } + + async fn geetest_ajax(ga: GeetestAjaxData) -> String { + println!("GeetestAjax: {:?}", ga); + + let is_next = match ga.BBF { + None => false, + Some(_) => true, + }; + + if (is_next) { + let callback = ga.callback.as_ref().unwrap(); + + return format!(" + {}( {{ + \"success\": 1, + \"message\": \"success\", + \"validate\": \"\", + \"score\": \"11\" + }} )", callback); + } else { + let data = " + { + \"status\": \"success\", + \"data\": { + \"result\": \"slide\" + } + } + "; + + return match ga.callback.as_ref() { + None => data.to_string(), + Some(callback) => format!( + "{}( + {} + )", + callback, data), + } + } + } + + async fn log_skip(body: web::Bytes) -> String { + //println!("Logging: {}", std::str::from_utf8(&body).unwrap()); + + return "{}".to_string(); + } + + async fn get_agreement_infos() -> String { + let payload = format!("{{ + \"marketing_agreements\": [] + }}"); + + return DispatchServer::make_answer(0, &payload); + } + + async fn compare_protocol_version() -> String { + let payload = format!("{{ + \"modified\": true, + \"protocol\": {{ + \"app_id\": 4, + \"create_time\": \"0\", + \"id\": 0, + \"language\": \"ru\", + \"major\": 4, + \"minimum\": 0, + \"priv_proto\": \"\", + \"teenager_proto\": \"\", + \"user_proto\": \"\" + }} + }}"); + + return DispatchServer::make_answer(0, &payload); + } + + async fn version_data() -> String { + return "{\"version\": 54}".to_string(); + } + + fn get_hostname() -> String { + return hostname::get().unwrap().into_string().unwrap(); + //return "localhost"; + } + + fn get_local_ip() -> String { + return local_ip_address::local_ip().unwrap().to_string(); + //return "127.0.0.1".to_string(); + } + fn load_keys(name: &str) -> (Vec, Vec) { // Key let filename = format!("./{}/{}.key", "keys", name); @@ -244,7 +656,7 @@ impl DispatchServer { let open_id = 0x1234; #[cfg(not(feature = "raw_packet_dump"))] - let combo_token = "Fake-token-hehehe"; + let combo_token = Self::generate_fake_token(); #[cfg(feature = "raw_packet_dump")] let combo_token = std::str::from_utf8(&[32u8; 4096*3]).unwrap(); @@ -253,7 +665,7 @@ impl DispatchServer { \"combo_id\": \"{}\", \"combo_token\": \"{}\", \"data\": {{\"guest\": \"false\"}}, - \"heartbeat\": \"false\", + \"heartbeat\": false, \"open_id\": \"{}\" }}", account_type, combo_id, combo_token, open_id); } @@ -262,7 +674,9 @@ impl DispatchServer { let payload = format!("{{ \"account\": {{ \"apple_name\": \"\", - \"country\": \"\", + \"area_code\": \"**\", + \"country\": \"US\", + \"device_grant_ticket\": \"\", \"email\": \"{}\", \"facebook_name\": \"\", \"game_center_name\": \"\", @@ -271,6 +685,7 @@ impl DispatchServer { \"is_email_verify\": \"0\", \"mobile\": \"\", \"name\": \"{}\", + \"reactivate_ticket\": \"\", \"realname\": \"\", \"safe_mobile\": \"\", \"sony_name\": \"\", @@ -281,6 +696,8 @@ impl DispatchServer { }}, \"device_grant_required\": \"false\", \"realperson_required\": \"false\", + \"realname_operation\": \"None\", + \"realperson_required\": false, \"safe_moblie_required\": \"false\" }}", email, name, token, uid); @@ -300,4 +717,12 @@ impl DispatchServer { \"data\": {} }}", code, message, data).to_string(); } + + fn generate_fake_token() -> String { + return rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect(); + } }