// Package sdkserver is the Go port of the Rust sdkserver: HTTP dispatch + // auth + SRTools save upload, listening on :21000. package sdkserver import ( "encoding/base64" "encoding/json" "fmt" "io" "net/http" "os" "sync" google "google.golang.org/protobuf/proto" goproto "gac-hkrpg-go/protocol/proto" ) const Port = 21000 // VersionConfig is one entry of versions.json. type VersionConfig struct { AssetBundleURL string `json:"asset_bundle_url"` ExResourceURL string `json:"ex_resource_url"` LuaURL string `json:"lua_url"` IfixURL string `json:"ifix_url"` } type appState struct { mu sync.Mutex hotfixMap map[string]VersionConfig } func loadHotfix() map[string]VersionConfig { const path = "versions.json" raw, err := os.ReadFile(path) if err != nil { return map[string]VersionConfig{} } out := map[string]VersionConfig{} if err := json.Unmarshal(raw, &out); err != nil { Log("malformed versions.json: " + err.Error()) return map[string]VersionConfig{} } return out } // getOrInsertHotfix mirrors AppState::get_or_insert_hotfix — unknown versions // are fetched from the official gateway via a proxy, then persisted. func (a *appState) getOrInsertHotfix(version, dispatchSeed string) VersionConfig { a.mu.Lock() defer a.mu.Unlock() if cfg, ok := a.hotfixMap[version]; ok { return cfg } Log(fmt.Sprintf("trying to fetch hotfix for version %s with dispatch seed %s", version, dispatchSeed)) hotfix, err := fetchHotfix(version, dispatchSeed) if err != nil { Log("failed to fetch hotfix. reason: " + err.Error()) hotfix = VersionConfig{} } a.hotfixMap[version] = hotfix if out, err := json.MarshalIndent(a.hotfixMap, "", " "); err == nil { _ = os.WriteFile("versions.json", out, 0o644) } return a.hotfixMap[version] } // fetchHotfix queries the official query_gateway endpoint through a proxy. func fetchHotfix(version, dispatchSeed string) (VersionConfig, error) { const ( proxyHost = "proxy1.neonteam.dev" osBeta = "beta-release01-asia.starrails.com" osProd = "prod-official-asia-dp01.starrails.com" cnBeta = "beta-release01-cn.bhsr.com" cnProd = "prod-gf-cn-dp01.bhsr.com" ) region, branch := parseVersionString(version) host := osBeta switch { case region == "OS" && branch == "PROD": host = osProd case region == "CN" && branch == "BETA": host = cnBeta case region == "CN" && branch == "PROD": host = cnProd } url := fmt.Sprintf("https://%s/%s/query_gateway?version=%s&platform_type=1&language_type=3&dispatch_seed=%s&channel_id=1&sub_channel_id=1&is_need_url=1", proxyHost, host, version, dispatchSeed) Log("fetching hotfix: " + url) resp, err := http.Get(url) if err != nil { return VersionConfig{}, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return VersionConfig{}, err } Log("raw gateway response: " + string(body)) bin, err := base64.StdEncoding.DecodeString(string(body)) if err != nil { return VersionConfig{}, err } decoded := &goproto.Gateserver{} if err := google.Unmarshal(bin, decoded); err != nil { return VersionConfig{}, err } if decoded.AssetBundleUrl == "" && decoded.ExResourceUrl == "" { return VersionConfig{}, fmt.Errorf("asset_bundle_url and ex_resource_url are empty! gateway response: %+v", decoded) } return VersionConfig{ AssetBundleURL: decoded.AssetBundleUrl, ExResourceURL: decoded.ExResourceUrl, LuaURL: decoded.LuaUrl, IfixURL: decoded.IfixUrl, }, nil } // parseVersionString extracts region/branch, e.g. "OSBETAWin4.6.51" -> OS,BETA. func parseVersionString(s string) (string, string) { branches := []string{"PREbeta", "BETA", "PROD", "DEV", "PRE", "GM", "CECREATION"} if len(s) < 2 { return "OS", "BETA" } region := s[:2] rest := s[2:] for _, b := range branches { if len(rest) >= len(b) && rest[:len(b)] == b { return region, b } } return region, "BETA" } // --------------------------------------------------------------------------- // handlers // --------------------------------------------------------------------------- func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(v) } func queryDispatch(w http.ResponseWriter, _ *http.Request) { rsp := &goproto.Dispatch{ Retcode: 0, RegionList: []*goproto.RegionInfo{{ Name: "GaC hkrpg GO", Title: "GaC hkrpg GO", EnvType: "9", DispatchUrl: fmt.Sprintf("http://127.0.0.1:%d/query_gateway", Port), }}, } bin, err := google.Marshal(rsp) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } _, _ = w.Write([]byte(base64.StdEncoding.EncodeToString(bin))) } func queryGateway(state *appState) http.HandlerFunc { type params struct { Version string `json:"-"` DispatchSeed string `json:"-"` } return func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() version := q.Get("version") seed := q.Get("dispatch_seed") _ = params{} cfg := state.getOrInsertHotfix(version, seed) rsp := &goproto.Gateserver{ Ip: "127.0.0.1", Port: 23301, AssetBundleUrl: cfg.AssetBundleURL, AssetBundleUrlB: cfg.AssetBundleURL, ExResourceUrl: cfg.ExResourceURL, LuaUrl: cfg.LuaURL, IfixVersion: "0", Unk1: true, Unk2: true, Unk3: true, Unk4: true, Unk5: true, Unk6: true, Unk7: true, } bin, err := google.Marshal(rsp) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } _, _ = w.Write([]byte(base64.StdEncoding.EncodeToString(bin))) } } type accountPayload struct { Data struct { Account map[string]any `json:"account"` } `json:"data"` Message string `json:"message"` Retcode int `json:"retcode"` } func standardAccount() map[string]any { return map[string]any{ "area_code": "**", "email": "ReversedRooms", "country": "RU", "is_email_verify": "1", "token": "mostsecuretokenever", "uid": "1337", } } func loginReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "data": map[string]any{ "account": standardAccount(), "device_grant_required": false, "reactivate_required": false, "realperson_required": false, "safe_mobile_required": false, }, "message": "OK", "retcode": 0, }) } func granterLoginReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "data": map[string]any{ "account_type": 1, "combo_id": "1337", "combo_token": "9065ad8507d5a1991cb6fddacac5999b780bbd92", "data": `{"guest":false}`, "heartbeat": false, "open_id": "1337", }, "message": "OK", "retcode": 0, }) } func riskyCheckReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "data": map[string]any{ "id": "06611ed14c3131a676b19c0d34c0644b", "action": "ACTION_NONE", "geetest": nil, }, "message": "OK", "retcode": 0, }) } func apnLoginReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "data": map[string]any{ "token": map[string]any{"token": "mostsecuretokenever", "token_type": 1}, "user_info": map[string]any{ "aid": "1337", "mid": "1337", "is_email_verify": 1, "area_code": "**", "country": "US", "is_adult": 1, "email": "motorized@wheel.chair", }, }, "message": "OK", "retcode": 0, }) } func apnVerifyReply(w http.ResponseWriter) { writeJSON(w, map[string]any{ "data": map[string]any{ "tokens": []any{map[string]any{"token": "mostsecuretokenever", "token_type": 1}}, "user_info": map[string]any{ "aid": "1337", "mid": "1337", "is_email_verify": 1, "area_code": "**", "country": "US", "is_adult": 1, "email": "motorized@wheel.chair", }, }, "message": "OK", "retcode": 0, }) } func notFound(w http.ResponseWriter, _ *http.Request) { writeJSON(w, map[string]any{"message": "OK", "retcode": 0}) } // srToolsSave is the /srtools upload endpoint (SRTools writes freesr-data.json). func srToolsSave(w http.ResponseWriter, r *http.Request) { var payload struct { Data json.RawMessage `json:"data"` } body, err := io.ReadAll(r.Body) if err == nil && len(body) > 0 { _ = json.Unmarshal(body, &payload) } if len(payload.Data) == 0 { writeJSON(w, map[string]any{"message": "OK", "status": 200}) return } // pretty-print and store; freesr-data.json uses the same layout the // gameserver parses, so pass through unchanged var pretty map[string]any if err := json.Unmarshal(payload.Data, &pretty); err != nil { writeJSON(w, map[string]any{"message": "malformed json: " + err.Error(), "status": 500}) return } out, _ := json.MarshalIndent(pretty, "", " ") if err := os.WriteFile("freesr-data.json", out, 0o644); err != nil { writeJSON(w, map[string]any{"message": "failed to write freesr-data.json: " + err.Error(), "status": 500}) return } writeJSON(w, map[string]any{"message": "OK", "status": 200}) } // Listen starts the HTTP sdkserver. func Listen() error { state := &appState{hotfixMap: loadHotfix()} Log(fmt.Sprintf("loaded %d hotfix versions", len(state.hotfixMap))) mux := http.NewServeMux() mux.HandleFunc("/query_dispatch", queryDispatch) mux.HandleFunc("/query_gateway", queryGateway(state)) mux.HandleFunc("/{product_name}/mdk/shield/api/login", func(w http.ResponseWriter, r *http.Request) { loginReply(w) }) mux.HandleFunc("/{product_name}/mdk/shield/api/verify", func(w http.ResponseWriter, r *http.Request) { loginReply(w) }) mux.HandleFunc("/{product_name}/combo/granter/login/v2/login", func(w http.ResponseWriter, r *http.Request) { granterLoginReply(w) }) mux.HandleFunc("/account/risky/api/check", func(w http.ResponseWriter, r *http.Request) { riskyCheckReply(w) }) mux.HandleFunc("/account/ma-cn-passport/app/loginByPassword", func(w http.ResponseWriter, r *http.Request) { apnLoginReply(w) }) mux.HandleFunc("/account/ma-cn-session/app/verify", func(w http.ResponseWriter, r *http.Request) { apnVerifyReply(w) }) mux.HandleFunc("/srtools", srToolsSave) mux.HandleFunc("/", notFound) addr := fmt.Sprintf("0.0.0.0:%d", Port) Log("sdkserver is listening at " + addr) return http.ListenAndServe(addr, mux) } func Log(msg string) { fmt.Println("[sdkserver]", msg) }