use axum::{ Json, extract::{Query, State}, response::IntoResponse, }; use base64::{Engine, display::Base64Display, engine::general_purpose::STANDARD}; use serde::{Deserialize, Serialize, Serializer}; use tracing::debug; use trigger_cryptography::rsa; use trigger_sv::config::RsaSetting; use crate::{ AppState, data::{ CdnConfExt, CdnDesignData, CdnGameRes, CdnSilenceData, RegionExtension, RegionSwitchFunc, ServerDispatchData, ServerGateway, }, }; pub const ROUTE_ENDPOINT: &str = "/query_gateway"; pub struct Response { pub data: ServerDispatchData<'static, 'static, 'static>, pub rsa: Option<&'static RsaSetting>, } #[derive(Debug, thiserror::Error)] pub enum QueryGatewayError { #[error("invalid rsa version specified: {0}")] InvalidRsaVer(u32), #[error("invalid dispatch seed, expected: {0}, got: {1}")] InvalidDispatchSeed(&'static str, String), } impl QueryGatewayError { pub fn retcode(&self) -> i32 { match self { Self::InvalidRsaVer(_) => 74, Self::InvalidDispatchSeed(_, _) => 75, } } pub fn message(&self) -> String { String::with_capacity(0) } } #[derive(Serialize)] struct EncryptedAndSignedData { #[serde(serialize_with = "to_base64")] pub content: Vec, #[serde(serialize_with = "to_base64")] pub sign: Vec, } impl IntoResponse for Response { fn into_response(self) -> axum::response::Response { let Some(rsa) = self.rsa else { return Json(self.data).into_response(); }; let json = serde_json::to_string(&self.data).unwrap(); Json(EncryptedAndSignedData { content: rsa::encrypt(&rsa.client_public_key, json.as_bytes()), sign: rsa::sign(&rsa.server_private_key, json.as_bytes()).into(), }) .into_response() } } #[derive(Deserialize)] pub struct QueryGatewayParam { pub rsa_ver: u32, pub seed: String, } pub async fn process( State(state): State<&'static AppState>, Query(param): Query, ) -> Response { match internal_process(&state, param) { Ok(response) => response, Err((rsa, err)) => { debug!("query_gateway failed: {err}"); Response { rsa, data: ServerDispatchData { retcode: err.retcode(), msg: err.message(), ..Default::default() }, } } } } fn internal_process( state: &'static AppState, param: QueryGatewayParam, ) -> Result, QueryGatewayError)> { use std::borrow::Cow::{Borrowed, Owned}; let rsa = state .environment .security .get_rsa_setting_by_version(param.rsa_ver); if rsa.is_none() { return Err((None, QueryGatewayError::InvalidRsaVer(param.rsa_ver))); }; (param.seed == state.config.bound_server.seed) .then_some(()) .ok_or(( rsa, QueryGatewayError::InvalidDispatchSeed(&state.config.bound_server.seed, param.seed), ))?; let server = &state.config.bound_server; Ok(Response { rsa, data: ServerDispatchData { retcode: 0, msg: String::with_capacity(0), region_name: Borrowed(&server.name), title: Borrowed(&server.title), client_secret_key: Owned( base64::engine::general_purpose::STANDARD .encode(&state.environment.security.static_key.seed_buf), ), cdn_check_url: String::with_capacity(0), gateway: Some(ServerGateway { ip: Borrowed(&server.addr), port: server.port, }), oaserver_url: String::new(), force_update_url: String::new(), stop_jump_url: String::new(), cdn_conf_ext: Some(CdnConfExt { // TODO: unhardcode this design_data: CdnDesignData { base_url: Borrowed( "https://autopatchcn.juequling.com/design_data/beta_live/output_7054632_323d17319c/client/", ), data_revision: Borrowed("7054632"), md5_files: Borrowed( r#"[{"fileName": "data_version", "fileSize": 4503, "fileMD5": "419987357302147246"}]"#, ), }, game_res: CdnGameRes { audio_revision: Borrowed("7025371"), base_url: Borrowed( "https://autopatchcn.juequling.com/game_res/beta_live/output_7054632_323d17319c/client/", ), branch: Borrowed("beta_live"), md5_files: Borrowed( r#"[{"fileName": "res_version", "fileSize": 2379030, "fileMD5": "15840336186912297231"}, {"fileName": "audio_version", "fileSize": 30435, "fileMD5": "15675397132378459243"}, {"fileName": "base_revision", "fileSize": 18, "fileMD5": "18079377284431001248"}]"#, ), res_revision: Borrowed("7054632"), }, silence_data: CdnSilenceData { base_url: Borrowed( "https://autopatchcn.juequling.com/design_data/beta_live/output_7054632_323d17319c/client_silence/", ), md5_files: Borrowed( r#"[{"fileName": "silence_version", "fileSize": 647, "fileMD5": "15019531890587528788"}]"#, ), silence_revision: Borrowed("7042559"), }, pre_download: None, }), region_ext: Some(RegionExtension { exchange_url: String::new(), feedback_url: String::new(), func_switch: RegionSwitchFunc { disable_frequent_attempts: 1, enable_gacha_mobile_console: 1, enable_notice_mobile_console: 1, enable_performance_log: 1, is_kcp: server.is_kcp as i32, ..Default::default() }, mtr_nap: String::new(), mtr_sdk: String::new(), pgc_webview_method: 1, url_check_nap: String::new(), url_check_sdk: String::new(), }), }, }) } pub fn to_base64(bytes: &[u8], serializer: S) -> Result where S: Serializer, { serializer.collect_str(&Base64Display::new(bytes, &STANDARD)) }