// Package sdkserver is the Go port of the Rust sdkserver: HTTP dispatch + // auth + SRTools save upload, using the shared runtime configuration. package sdkserver import ( "encoding/base64" "encoding/json" "fmt" "io" "net/http" "os" "sync" "gac-hkrpg-go/internal/common" google "google.golang.org/protobuf/proto" goproto "gac-hkrpg-go/protocol/proto" ) // 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 { runtime common.RuntimeConfig mu sync.Mutex hotfixMap map[string]VersionConfig } func loadHotfix() map[string]VersionConfig { out := map[string]VersionConfig{} if raw, err := os.ReadFile("hotfix.json"); err == nil { _ = json.Unmarshal(raw, &out) } if raw, err := os.ReadFile("versions.json"); err == nil { _ = json.Unmarshal(raw, &out) } 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 } if cfg, ok := a.hotfixMap["CNBETAWin"+version]; ok { return cfg } if cfg, ok := a.hotfixMap["OSBETAWin"+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. a version with the OSBETAWin prefix -> 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, runtime common.RuntimeConfig) { rsp := &goproto.Dispatch{ Retcode: 0, RegionList: []*goproto.RegionInfo{{ Name: "GaC hkrpg GO", Title: "GaC hkrpg GO", EnvType: "9", DispatchUrl: runtime.PublicSDKURL + "/query_gateway", }}, } 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: state.runtime.PublicGameHost, Port: uint32(state.runtime.GamePort), 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 { writeJSON(w, map[string]any{"message": "empty body", "status": 400}) return } toWrite := body if err := json.Unmarshal(body, &payload); err == nil && len(payload.Data) > 0 { toWrite = payload.Data } // 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(toWrite, &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 } // Reload and sync persistent if d, err := common.LoadFreesrData(); err == nil { d.SavePersistent() } writeJSON(w, map[string]any{"message": "OK", "status": 200}) } // Listen starts the HTTP sdkserver. func Listen() error { runtime, err := common.LoadRuntimeConfig() if err != nil { return err } state := &appState{hotfixMap: loadHotfix(), runtime: runtime} Log(fmt.Sprintf("loaded %d hotfix versions", len(state.hotfixMap))) mux := http.NewServeMux() mux.HandleFunc("/query_dispatch", func(w http.ResponseWriter, r *http.Request) { queryDispatch(w, r, runtime) }) 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", corsWrapper(srToolsSave)) registerAPIRoutes(mux) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" || r.URL.Path == "/dashboard" { apiRoot(w, r) return } notFound(w, r) }) addr := runtime.SDKAddress() Log("sdkserver is listening at " + addr) Log(fmt.Sprintf("REST API Gateway is live at %s/ (for GaC-Srtool)", runtime.PublicSDKURL)) return http.ListenAndServe(addr, mux) } func Log(msg string) { fmt.Println("[sdkserver]", msg) }