Compare commits

..

No commits in common. "main" and "3.1.53" have entirely different histories.
main ... 3.1.53

5 changed files with 44 additions and 200 deletions

View File

@ -11,7 +11,6 @@ tokio = { version = "1.36.0", features = ["full"] }
axum = "0.8.1" axum = "0.8.1"
axum-server = "0.7.1" axum-server = "0.7.1"
tower-http = "0.6.2" tower-http = "0.6.2"
reqwest = "0.12.12"
# JSON # JSON
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }

View File

@ -9,7 +9,6 @@ tokio.workspace = true
tower-http = { workspace = true, features = ["cors"] } tower-http = { workspace = true, features = ["cors"] }
axum.workspace = true axum.workspace = true
axum-server.workspace = true axum-server.workspace = true
reqwest.workspace = true
# JSON # JSON
serde.workspace = true serde.workspace = true

View File

@ -1,127 +1,27 @@
use std::collections::HashMap; use std::{collections::HashMap, sync::OnceLock};
use anyhow::{Result, anyhow}; use serde::Deserialize;
use prost::Message as _;
use proto::Gateserver;
use serde::{Deserialize, Serialize};
use tokio::fs;
const DEFAULT_VERSIONS: &str = include_str!("../../../versions.json"); const DEFAULT_VERSIONS: &str = include_str!("../../../versions.json");
#[derive(Serialize, Deserialize, Clone, Default)] #[derive(Deserialize)]
pub struct VersionConfig { pub struct VersionConfig {
pub asset_bundle_url: String, pub asset_bundle_url: String,
pub ex_resource_url: String, pub ex_resource_url: String,
pub lua_url: String, pub lua_url: String,
pub ifix_url: String, // pub lua_version: String,
} }
impl VersionConfig { pub fn instance() -> &'static HashMap<String, VersionConfig> {
const CNPROD_HOST: &str = "prod-gf-cn-dp01.bhsr.com"; static INSTANCE: OnceLock<HashMap<String, VersionConfig>> = OnceLock::new();
const CNBETA_HOST: &str = "beta-release01-cn.bhsr.com"; INSTANCE.get_or_init(|| {
const OSPROD_HOST: &str = "prod-official-asia-dp01.starrails.com";
const OSBETA_HOST: &str = "beta-release01-asia.starrails.com";
const PROXY_HOST: &str = "proxy1.neonteam.dev"; // we used this because PS users usually have a proxy enabled
pub async fn load_hotfix() -> HashMap<String, VersionConfig> {
const CONFIG_PATH: &str = "versions.json"; const CONFIG_PATH: &str = "versions.json";
let data = match fs::read_to_string(CONFIG_PATH).await { let data = std::fs::read_to_string(CONFIG_PATH).unwrap_or_else(|_| {
Ok(data) => data, std::fs::write(CONFIG_PATH, DEFAULT_VERSIONS).expect("Failed to create versions file");
Err(_) => {
fs::write(CONFIG_PATH, DEFAULT_VERSIONS)
.await
.expect("Failed to create versions file");
DEFAULT_VERSIONS.to_string() DEFAULT_VERSIONS.to_string()
} });
};
match serde_json::from_str(&data) { serde_json::from_str(&data).unwrap_or_else(|e| panic!("Failed to parse versions.json: {e}"))
Ok(data) => data,
Err(_) => {
tracing::error!("malformed versions.json. replacing it with default one");
let _ = fs::write(CONFIG_PATH, DEFAULT_VERSIONS).await;
serde_json::from_str(DEFAULT_VERSIONS).unwrap()
}
}
}
pub async fn fetch_hotfix(version: &str, dispatch_seed: &str) -> Result<VersionConfig> {
let (region, branch, _, _) = Self::parse_version_string(version)?;
let host = match (region.as_str(), branch.as_str()) {
("OS", "BETA") => Self::OSBETA_HOST,
("OS", "PROD") => Self::OSPROD_HOST,
("CN", "BETA") => Self::CNBETA_HOST,
("CN", "PROD") => Self::CNPROD_HOST,
// TODO: Support more host, or use query_dispatch result to determine the urls
_ => Self::OSBETA_HOST,
};
let url = format!(
"https://{}/{host}/query_gateway?version={}&platform_type=1&language_type=3&dispatch_seed={}&channel_id=1&sub_channel_id=1&is_need_url=1",
Self::PROXY_HOST,
version,
dispatch_seed
);
tracing::info!("fetching hotfix: {url}");
let res = reqwest::get(url).await?.text().await?;
tracing::info!("raw gateway response: {}", res);
let bytes = rbase64::decode(&res)?;
let decoded = Gateserver::decode(bytes.as_slice())?;
if decoded.retcode != 0 || res.is_empty() {
return Err(anyhow::format_err!(
"gateway result code: {} message: {}",
decoded.retcode,
decoded.msg
));
}
tracing::info!("{:#?}", decoded);
Ok(VersionConfig {
asset_bundle_url: decoded.asset_bundle_url,
ex_resource_url: decoded.ex_resource_url,
lua_url: decoded.lua_url,
ifix_url: decoded.ifix_url,
}) })
} }
fn parse_version_string(s: &str) -> Result<(String, String, String, String)> {
const BRANCHES: [&str; 7] = ["PREbeta", "BETA", "PROD", "DEV", "PRE", "GM", "CECREATION"];
const OS: [&str; 3] = ["Android", "Win", "iOS"];
let region = s
.get(0..2)
.ok_or_else(|| anyhow!("version parse failed. reason: invalid region url: {s}"))?;
let after_region = s.get(2..).unwrap_or(s);
let branch = BRANCHES
.iter()
.find(|&&b| after_region.starts_with(b))
.map(|&b| b.to_string())
.ok_or_else(|| anyhow!("version parse failed. reason: invalid branch url: {s}"))?;
let branch_len = branch.len();
let after_branch = after_region.get(branch_len..).unwrap_or(after_region);
let os = OS
.iter()
.find(|&&o| after_branch.starts_with(o))
.map(|&o| o.to_string())
.ok_or_else(|| anyhow!("version parse failed. reason: invalid os url: {s}"))?;
let os_len = os.len();
let version = after_branch
.get(os_len..)
.ok_or_else(|| anyhow!("version parse failed. reason: invalid version url: {s}"))?;
Ok((region.to_string(), branch, os, version.to_string()))
}
}

View File

@ -1,15 +1,9 @@
use std::collections::HashMap;
use std::sync::Arc;
use anyhow::Result; use anyhow::Result;
use axum::Router; use axum::Router;
use axum::http::Method; use axum::http::Method;
use axum::http::header::CONTENT_TYPE; use axum::http::header::CONTENT_TYPE;
use axum::routing::{get, post}; use axum::routing::{get, post};
use config::version_config::VersionConfig;
use services::{auth, dispatch, errors, sr_tools}; use services::{auth, dispatch, errors, sr_tools};
use tokio::fs;
use tokio::sync::RwLock;
use tower_http::cors::{Any, CorsLayer}; use tower_http::cors::{Any, CorsLayer};
use tracing::Level; use tracing::Level;
@ -18,51 +12,9 @@ mod services;
const PORT: u16 = 21000; const PORT: u16 = 21000;
#[derive(Clone)]
struct AppState {
hotfix_map: HashMap<String, VersionConfig>,
}
impl AppState {
async fn get_or_insert_hotfix(&mut self, version: &str, dispatch_seed: &str) -> &VersionConfig {
if self.hotfix_map.contains_key(version) {
return &self.hotfix_map[version];
}
tracing::info!(
"trying to fetch hotfix for version {version} with dispatch seed {dispatch_seed}"
);
let hotfix = match VersionConfig::fetch_hotfix(version, dispatch_seed).await {
Ok(hotfix) => hotfix,
Err(err) => {
tracing::error!("failed to fetch hotfix. reason: {err}");
VersionConfig::default()
}
};
self.hotfix_map.insert(version.to_string(), hotfix);
if let Ok(serialized) = serde_json::to_string_pretty(&self.hotfix_map) {
let _ = fs::write("versions.json", serialized).await;
}
&self.hotfix_map[version]
}
}
pub async fn start_sdkserver() -> Result<()> { pub async fn start_sdkserver() -> Result<()> {
let span = tracing::span!(Level::DEBUG, "main"); let span = tracing::span!(Level::DEBUG, "main");
let _ = span.enter(); let _ = span.enter();
let hotfix_map = VersionConfig::load_hotfix().await;
tracing::info!(
"loaded {} hotfix versions. supported versions: {:?}",
hotfix_map.len(),
hotfix_map.keys()
);
let state = Arc::new(RwLock::new(AppState { hotfix_map }));
let router = Router::new() let router = Router::new()
.route( .route(
@ -96,7 +48,6 @@ pub async fn start_sdkserver() -> Result<()> {
.allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE]) .allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
.allow_headers([CONTENT_TYPE]), .allow_headers([CONTENT_TYPE]),
) )
.with_state(state)
.fallback(errors::not_found); .fallback(errors::not_found);
let addr = format!("0.0.0.0:{PORT}"); let addr = format!("0.0.0.0:{PORT}");

View File

@ -1,12 +1,8 @@
use std::sync::Arc; use crate::config::version_config;
use axum::extract::Query;
use crate::AppState;
use axum::extract::{Query, State};
use prost::Message; use prost::Message;
use proto::{DispatchRegionData, Gateserver, RegionEntry}; use proto::{DispatchRegionData, Gateserver, RegionEntry};
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::RwLock;
use tracing::instrument;
pub const QUERY_DISPATCH_ENDPOINT: &str = "/query_dispatch"; pub const QUERY_DISPATCH_ENDPOINT: &str = "/query_dispatch";
pub const QUERY_GATEWAY_ENDPOINT: &str = "/query_gateway"; pub const QUERY_GATEWAY_ENDPOINT: &str = "/query_gateway";
@ -34,20 +30,12 @@ pub async fn query_dispatch() -> String {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct QueryGatewayParameters { pub struct QueryGatewayParameters {
pub version: String, pub version: String,
pub dispatch_seed: String,
} }
#[instrument(skip(state))] #[tracing::instrument]
pub async fn query_gateway( pub async fn query_gateway(parameters: Query<QueryGatewayParameters>) -> String {
State(state): State<Arc<RwLock<AppState>>>, let rsp = if let Some(config) = version_config::instance().get(&parameters.version) {
parameters: Query<QueryGatewayParameters>, Gateserver {
) -> String {
let mut lock = state.write().await;
let config = lock
.get_or_insert_hotfix(&parameters.version, &parameters.dispatch_seed)
.await;
let rsp = Gateserver {
retcode: 0, retcode: 0,
ip: String::from("127.0.0.1"), ip: String::from("127.0.0.1"),
port: 23301, port: 23301,
@ -66,6 +54,13 @@ pub async fn query_gateway(
enable_cdn_ipv6: 1, enable_cdn_ipv6: 1,
enable_save_replay_file: true, enable_save_replay_file: true,
..Default::default() ..Default::default()
}
} else {
Gateserver {
retcode: 9,
login_white_msg: format!("forbidden version: {} or invalid bind", parameters.version),
..Default::default()
}
}; };
let mut buff = Vec::new(); let mut buff = Vec::new();